diff --git a/HISTORY.md b/HISTORY.md index df296770bc..56aa712533 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,9 +2,13 @@ ## 1.21.0 (2022-09-28) +AEA: +- Updates `aea scaffold contract` to include contract ABIs + Packages: - Adds support for running local ACN nodes - Converts `ledger` and `http_client` connections and `http`, `ledger_api` and `contract_api` protocols to valory packages and syncs them with `open-autonomy` versions of the same packages +- Extends `ledger` connection to automatically handle contract calls to methods not implemented in the contract package, redirecting them to a default contract method. Plugins: - Introduces test tools module for IPFS cli plugin @@ -14,17 +18,20 @@ Tests: - Fixes flaky `DHT (ACN/Libp2p)` tests on windows - Introduces test for assessing robustness of the ACN setup without agents +Docs: +- Adds a guide on implementing contract packages + ## 1.20.0 (2022-09-20) AEA: - Ensures author and year in copyright headers are updated in scaffolded components -- Updates `check-packages` +- Updates `check-packages` - to check the presence of the constant `PUBLIC_ID` for connections and skills. - to validate author - Fixes CLI help message for `aea config set` command - Extends test command to support consistency check skips and to run tests for a specific author -- Adds proper exception raising and error handling +- Adds proper exception raising and error handling - Exception handling when downloading from IPFS nodes - Better error message when the `--aev` flag is not provided - Fixes file sorting to maintain consistency of links on `PBNode` object on `IPFSHashOnly` tool @@ -43,15 +50,15 @@ Chores: - Fixes docstring formatting to make sure doc generator works fine - Updated `check_ipfs_hashes.py` script to use `packages.json` instead of `hashes.csv` - Updates the command regex to align with the latest version - + ## 1.19.0 (2022-09-14) AEA: - Updates the `aea init` command to set the local as default registry and IPFS as default remote registry - Updates the `aea test packages` to include the agent tests -- Introduces +- Introduces - `aea packages` command group to manage local packages repository - - `aea packages lock` command to lock all available packages and create `packages.json` file + - `aea packages lock` command to lock all available packages and create `packages.json` file - `aea packages sync` command to synchronize the local packages repository Chores: diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index 2186405353..592514fe5c 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -24,7 +24,7 @@ import shutil from datetime import datetime from pathlib import Path -from typing import cast +from typing import Optional, cast import click from jsonschema import ValidationError @@ -39,10 +39,12 @@ create_symlink_vendor_to_local, validate_package_name, ) -from aea.configurations.base import PublicId +from aea.configurations.base import PackageType, PublicId from aea.configurations.constants import ( # noqa: F401 # pylint: disable=unused-import + BUILD, CONNECTION, CONTRACT, + CONTRACTS, DEFAULT_AEA_CONFIG_FILE, DEFAULT_CONNECTION_CONFIG_FILE, DEFAULT_CONTRACT_CONFIG_FILE, @@ -53,7 +55,9 @@ PROTOCOL, SCAFFOLD_PUBLIC_ID, SKILL, + _ETHEREUM_IDENTIFIER, ) +from aea.configurations.loader import ConfigLoader from aea.helpers.io import open_file from aea.helpers.ipfs.base import IPFSHashOnly @@ -137,10 +141,17 @@ def connection(ctx: Context, connection_name: str) -> None: @scaffold.command() @click.argument("contract_name", type=str, required=True) +@click.argument( + "contract_abi_path", type=click.Path(dir_okay=True, exists=True), required=False +) @pass_ctx -def contract(ctx: Context, contract_name: str) -> None: +def contract( + ctx: Context, contract_name: str, contract_abi_path: Optional[Path] = None +) -> None: """Add a contract scaffolding to the configuration file and agent.""" scaffold_item(ctx, CONTRACT, contract_name) + if contract_abi_path: + add_contract_abi(ctx, contract_name, Path(contract_abi_path)) @scaffold.command() @@ -360,3 +371,52 @@ def _scaffold_non_package_item( except Exception as e: os.remove(dest) raise click.ClickException(str(e)) + + +def add_contract_abi(ctx: Context, contract_name: str, contract_abi_path: Path) -> None: + """ + Add the contract ABI to a contract scaffold. + + :param ctx: the CLI context. + :param contract_name: the contract name. + :param contract_abi_path: the contract ABI path. + """ + + # Get some data from the context + to_local_registry = ctx.config.get("to_local_registry") + author_name = ctx.agent_config.author + + # Create the build directory and copy the ABI file + contract_dir_root = ( + Path(str(ctx.agent_config.directory)) if to_local_registry else Path() + ) + + contract_dir = contract_dir_root / Path(CONTRACTS) / Path(contract_name) + abi_dest = contract_dir / Path(BUILD) / Path(contract_abi_path.name) + + click.echo(f"Updating contract scaffold '{contract_name}' to include ABI...") + + os.makedirs(os.path.dirname(abi_dest), exist_ok=True) + shutil.copyfile(str(contract_abi_path), abi_dest) + + # Load configuration (contract.yaml) + config_file = Path(contract_dir / DEFAULT_CONTRACT_CONFIG_FILE) + config_loader = ConfigLoader.from_configuration_type(PackageType.CONTRACT) + contract_config = config_loader.load(config_file.open("r")) + + # Add ABI to the configuration + # Can't use contract_config.update() here. Since contract_interface_paths is empty during instantiation, + # it cannot be validated afterwards even if we add the correct values to ContractConfig.FIELDS_ALLOWED_TO_UPDATE + contract_config.contract_interface_paths = { + _ETHEREUM_IDENTIFIER: f"{BUILD}/{contract_abi_path.name}" + } + + # Write the new configuration + config_loader.dump(contract_config, config_file.open("w+")) + + # Fingerprint again + new_public_id = PublicId(author_name, contract_name, DEFAULT_VERSION) + + if to_local_registry: + ctx.cwd = str(ctx.agent_config.directory) + fingerprint_item(ctx, CONTRACT, new_public_id) diff --git a/aea/configurations/constants.py b/aea/configurations/constants.py index 518512eade..b82c84149e 100644 --- a/aea/configurations/constants.py +++ b/aea/configurations/constants.py @@ -45,6 +45,7 @@ VENDOR = "vendor" AGENT = "agent" AGENTS = "agents" +BUILD = "build" CONNECTION = "connection" CONNECTIONS = "connections" CONTRACT = "contract" diff --git a/aea/contracts/base.py b/aea/contracts/base.py index 0cfa9f1d28..45d672ab27 100644 --- a/aea/contracts/base.py +++ b/aea/contracts/base.py @@ -257,6 +257,31 @@ def build_transaction( ) return tx + @classmethod + def default_method_call( + cls, + ledger_api: LedgerApi, + contract_address: str, + method_name: str, + **kwargs: Any, + ) -> Optional[JSONLike]: + """ + Make a contract call. + + :param ledger_api: the ledger apis. + :param contract_address: the contract address. + :param method_name: the method to call. + :param kwargs: keyword arguments. + :return: the call result + """ + + contract_instance = cls.get_instance(ledger_api, contract_address) + + result = ledger_api.contract_method_call( + contract_instance, method_name, **kwargs + ) + return result + @classmethod def get_transaction_transfer_logs( cls, diff --git a/docs/api/contracts/base.md b/docs/api/contracts/base.md index f52cad1cff..a17f6cfa10 100644 --- a/docs/api/contracts/base.md +++ b/docs/api/contracts/base.md @@ -270,6 +270,29 @@ Build a transaction. the transaction + + +#### default`_`method`_`call + +```python +@classmethod +def default_method_call(cls, ledger_api: LedgerApi, contract_address: str, + method_name: str, **kwargs: Any) -> Optional[JSONLike] +``` + +Make a contract call. + +**Arguments**: + +- `ledger_api`: the ledger apis. +- `contract_address`: the contract address. +- `method_name`: the method to call. +- `kwargs`: keyword arguments. + +**Returns**: + +the call result + #### get`_`transaction`_`transfer`_`logs diff --git a/docs/api/plugins/aea_cli_ipfs/test_tools/fixture_helpers.md b/docs/api/plugins/aea_cli_ipfs/test_tools/fixture_helpers.md new file mode 100644 index 0000000000..e3ef9463f6 --- /dev/null +++ b/docs/api/plugins/aea_cli_ipfs/test_tools/fixture_helpers.md @@ -0,0 +1,17 @@ + + +# plugins.aea-cli-ipfs.aea`_`cli`_`ipfs.test`_`tools.fixture`_`helpers + +Fixture helpers. + + + +#### ipfs`_`daemon + +```python +@pytest.fixture(scope="module") +def ipfs_daemon() -> Iterator[bool] +``` + +Starts an IPFS daemon for the tests. + diff --git a/packages/fetchai/skills/erc1155_client/skill.yaml b/packages/fetchai/skills/erc1155_client/skill.yaml index 01af4c5282..8b65c5ba35 100644 --- a/packages/fetchai/skills/erc1155_client/skill.yaml +++ b/packages/fetchai/skills/erc1155_client/skill.yaml @@ -21,7 +21,7 @@ fingerprint: tests/test_strategy.py: bafybeicbxie3v6vue3gcnru6vsvggcgy3shxwrldis5gppizbuhooslcqa fingerprint_ignore_patterns: [] connections: -- valory/ledger:0.19.0:bafybeigpltrga4ggf4nejvl7l32zioyk77jzodvhthjwd3uvdkuxedvnz4 +- valory/ledger:0.19.0:bafybeic34ll726rlh4bwaeiuzmw5aoiw54ikvdgznlc5q2njv43affylpe contracts: - fetchai/erc1155:0.22.0:bafybeidw2vhsh3ifmg5sxnbxhpu4ygosov5jtzjhilsfkdhoanvwiu7dyi protocols: diff --git a/packages/fetchai/skills/erc1155_deploy/skill.yaml b/packages/fetchai/skills/erc1155_deploy/skill.yaml index a33f42428c..a780199d8d 100644 --- a/packages/fetchai/skills/erc1155_deploy/skill.yaml +++ b/packages/fetchai/skills/erc1155_deploy/skill.yaml @@ -21,7 +21,7 @@ fingerprint: tests/test_strategy.py: bafybeigxtw2j2c7vl6xhdwos62jbtmx62xfgdyadptm5eewmkesmcooyea fingerprint_ignore_patterns: [] connections: -- valory/ledger:0.19.0:bafybeigpltrga4ggf4nejvl7l32zioyk77jzodvhthjwd3uvdkuxedvnz4 +- valory/ledger:0.19.0:bafybeic34ll726rlh4bwaeiuzmw5aoiw54ikvdgznlc5q2njv43affylpe contracts: - fetchai/erc1155:0.22.0:bafybeidw2vhsh3ifmg5sxnbxhpu4ygosov5jtzjhilsfkdhoanvwiu7dyi protocols: diff --git a/packages/fetchai/skills/generic_buyer/skill.yaml b/packages/fetchai/skills/generic_buyer/skill.yaml index a70c1aeddb..99dd773176 100644 --- a/packages/fetchai/skills/generic_buyer/skill.yaml +++ b/packages/fetchai/skills/generic_buyer/skill.yaml @@ -19,7 +19,7 @@ fingerprint: tests/test_models.py: bafybeibh72j3n72yseqvmpppucpu5wtidf6ebxbxkfnmrnlh4zv5y5apei fingerprint_ignore_patterns: [] connections: -- valory/ledger:0.19.0:bafybeigpltrga4ggf4nejvl7l32zioyk77jzodvhthjwd3uvdkuxedvnz4 +- valory/ledger:0.19.0:bafybeic34ll726rlh4bwaeiuzmw5aoiw54ikvdgznlc5q2njv43affylpe contracts: [] protocols: - fetchai/default:1.0.0:bafybeihzesahyayexkhk26fg7rqnjuqaab3bmcijtjekvskvs4xw6ecyuu diff --git a/packages/fetchai/skills/generic_seller/skill.yaml b/packages/fetchai/skills/generic_seller/skill.yaml index c13a5cb9b3..9f493670e9 100644 --- a/packages/fetchai/skills/generic_seller/skill.yaml +++ b/packages/fetchai/skills/generic_seller/skill.yaml @@ -20,7 +20,7 @@ fingerprint: tests/test_models.py: bafybeihabrc22zqssit3fmqhxptosy6qz6mx65ukhf5iayvirfv42xrhoq fingerprint_ignore_patterns: [] connections: -- valory/ledger:0.19.0:bafybeigpltrga4ggf4nejvl7l32zioyk77jzodvhthjwd3uvdkuxedvnz4 +- valory/ledger:0.19.0:bafybeic34ll726rlh4bwaeiuzmw5aoiw54ikvdgznlc5q2njv43affylpe contracts: [] protocols: - fetchai/default:1.0.0:bafybeihzesahyayexkhk26fg7rqnjuqaab3bmcijtjekvskvs4xw6ecyuu diff --git a/packages/packages.json b/packages/packages.json index 0f87ccef03..e91106ebdd 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -8,7 +8,7 @@ "protocol/valory/ledger_api/1.0.0": "bafybeih7rhi5zvfvwakx5ifgxsz2cfipeecsh7bm3gnudjxtvhrygpcftq", "connection/fetchai/http_server/0.22.0": "bafybeifbdjgzroxywqu5bimvj373yduhnc5n27vxbjf3fqs3mx7s6a5etq", "connection/fetchai/stub/0.21.0": "bafybeieqlozydyvdxmjxhqygwq27djecpiftoqwlcpcr4qpotomwnh66yy", - "connection/valory/ledger/0.19.0": "bafybeigpltrga4ggf4nejvl7l32zioyk77jzodvhthjwd3uvdkuxedvnz4", + "connection/valory/ledger/0.19.0": "bafybeic34ll726rlh4bwaeiuzmw5aoiw54ikvdgznlc5q2njv43affylpe", "connection/valory/p2p_libp2p/0.1.0": "bafybeibjirmffnyih5gplt2uu6ojvta25w7up6yt447unungn4xpgqn5ca", "connection/valory/p2p_libp2p_client/0.1.0": "bafybeihf35zfr35qsvfte4vbi7njvuzfx4httysw7owmlux53gvxh2or54", "connection/valory/p2p_libp2p_mailbox/0.1.0": "bafybeibptb6lzhyknd265jc2n33r25g4a6fuhmh4pdmkmf4dy4o5pkv6vi", @@ -31,11 +31,11 @@ "connection/valory/http_client/0.23.0": "bafybeihz3tubwado7j3wlivndzzuj3c6fdsp4ra5r3nqixn3ufawzo3wii", "connection/valory/test_libp2p/0.1.0": "bafybeiapa25o6vzyuyvdcfad7bgvhknaa2he6rkyp2sdicne7j446hj6ya", "protocol/fetchai/tac/1.0.0": "bafybeiaew226n32rwp3h57zl4b2mmbrhjbyrdjbl2evnxf2tmmi4vrls7a", - "skill/fetchai/erc1155_client/0.28.0": "bafybeiaxla5l367xokpjjtnqt5z5jfcghafelrxsvr73kbavhn72wfazsi", - "skill/fetchai/erc1155_deploy/0.30.0": "bafybeieu4zmaxvvnldetqt6656wxdrxzo6r7wmix3wwjtshhnvdsojozbe", + "skill/fetchai/erc1155_client/0.28.0": "bafybeigcmxyzdblcbhsnzptyj2hgcjhfmvdelu5mfguumasoc3kbq72ufy", + "skill/fetchai/erc1155_deploy/0.30.0": "bafybeigj7ymiq6mjkhjxhabf5gj42u3xcl2nqwxi23henx7hdsz2tmno6y", "skill/fetchai/error/0.17.0": "bafybeib7nhokw3bc46oxuk5mjazan42evipowmka2ikfcs6drcdz4mwkjm", "skill/fetchai/fipa_dummy_buyer/0.2.0": "bafybeiha4jultg5srhr2ijplvubeo7esv4raq2cjlggmyzcaimop2ggg2m", - "skill/fetchai/generic_buyer/0.26.0": "bafybeibeif2qi7ckrt7hiv3ejt4wszblhmqiztomegebxlkoc625msnn7i", - "skill/fetchai/generic_seller/0.27.0": "bafybeifxkhcyll4skuooap7mukqpnvivtypovlr7pyrc3ieads3ya2v3ji", + "skill/fetchai/generic_buyer/0.26.0": "bafybeigjobpo33znfokdktmrbobnh3ygzvb54d6bpcjpdll3ooomd66whm", + "skill/fetchai/generic_seller/0.27.0": "bafybeiflkay5u3nrxdysob6vjzee6w5dznr2hnbwxnyqgwfcykcyf7c3qa", "skill/fetchai/task_test_skill/0.1.0": "bafybeidv77u2xl52mnxakwvh7fuh46aiwfpteyof4eaptfd4agoi6cdble" } \ No newline at end of file diff --git a/packages/valory/connections/ledger/connection.yaml b/packages/valory/connections/ledger/connection.yaml index cdf28ff5bd..be553a05d4 100644 --- a/packages/valory/connections/ledger/connection.yaml +++ b/packages/valory/connections/ledger/connection.yaml @@ -10,7 +10,7 @@ fingerprint: __init__.py: bafybeierqitcqk7oy6m3qp7jgs67lcg55mzt3arltkwimuii2ynfejccwi base.py: bafybeicpyhus3h2t5urzldnjns2sfwae64uinethqnlunudclbdg4xftnq connection.py: bafybeiehfn2chbgeat5mj23mcelfrfifiezvrwwucdpaz7ku2ygo7dxd5y - contract_dispatcher.py: bafybeiatma5jzq3ynh52t5r7ibq4k6qzs3c5opkf4ct67qkmq7l2mccmxq + contract_dispatcher.py: bafybeigqgqe6zef335t2ygp4celx7445etwjsr42yroc2qmrynwfslgjhq ledger_dispatcher.py: bafybeibh2d5qgj76stlkiiynu2irvcohntf27f3zrqwlkxwz5zjz67u4qm tests/__init__.py: bafybeieyhttiwruutk6574yzj7dk2afamgdum5vktyv54gsax7dlkuqtc4 tests/conftest.py: bafybeihqsdoamxlgox2klpjwmyrylrycyfon3jldvmr24q4ai33h24llpi diff --git a/packages/valory/connections/ledger/contract_dispatcher.py b/packages/valory/connections/ledger/contract_dispatcher.py index 75ff59e434..4fb3013bc2 100644 --- a/packages/valory/connections/ledger/contract_dispatcher.py +++ b/packages/valory/connections/ledger/contract_dispatcher.py @@ -26,6 +26,7 @@ from aea.common import JSONLike from aea.contracts import Contract, contract_registry +from aea.contracts.base import snake_to_camel from aea.crypto.base import LedgerApi from aea.crypto.registries import Registry from aea.exceptions import AEAException, parse_exception @@ -357,12 +358,32 @@ def _validate_and_call_callable( :param contract: the contract instance. :return: the data generated by the method. """ + method_to_call: Optional[Callable] = None try: method_to_call = getattr(contract, message.callable) - except AttributeError as exception: - raise AEAException( - f"Cannot find {message.callable} in contract {type(contract)}" - ) from exception + except AttributeError: + _default_logger.info( + f"Contract method {message.callable} not found in the contract package {contract.contract_id}. Checking in the ABI..." + ) + + # Check for the method in the ABI + if not method_to_call: + try: + contract_instance = contract.get_instance(api, message.contract_address) + contract_instance.get_function_by_name(message.callable) + except ValueError: + raise AEAException( + f"Contract method {message.callable} not found in ABI of contract {type(contract)}" + ) + + default_method_call = contract.default_method_call + return default_method_call( # type: ignore + api, + message.contract_address, + snake_to_camel(message.callable), + **message.kwargs.body, + ) + full_args_spec = inspect.getfullargspec(method_to_call) if message.performative in [ ContractApiMessage.Performative.GET_STATE, diff --git a/scripts/whitelist.py b/scripts/whitelist.py index dea71a52ef..3eea61e66d 100644 --- a/scripts/whitelist.py +++ b/scripts/whitelist.py @@ -325,3 +325,4 @@ remove_test_directory # unused function (aea/test_tools/utils.py:44) execinfo # unused variable (aea/test_tools/utils.py:55) ACNWithBootstrappedEntryNodesDockerImage # unused class (aea/test_tools/acn_image.py:166) +default_method_call # unused method (aea/contracts/base.py:260) diff --git a/tests/test_contracts/IUniswapV2ERC20.json b/tests/test_contracts/IUniswapV2ERC20.json new file mode 100644 index 0000000000..01003d647f --- /dev/null +++ b/tests/test_contracts/IUniswapV2ERC20.json @@ -0,0 +1,693 @@ +{ + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ], + "evm": { + "bytecode": { + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + }, + "deployedBytecode": { + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + } + }, + "interface": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "" +} \ No newline at end of file diff --git a/tests/test_contracts/test_base.py b/tests/test_contracts/test_base.py index 8611f77df3..3e15b709c9 100644 --- a/tests/test_contracts/test_base.py +++ b/tests/test_contracts/test_base.py @@ -19,11 +19,14 @@ # ------------------------------------------------------------------------------ """This module contains tests for aea.contracts.base.""" - import logging import os +import shutil +from dataclasses import dataclass from pathlib import Path +from tempfile import mkdtemp from typing import cast +from unittest import mock from unittest.mock import MagicMock import pytest @@ -32,7 +35,13 @@ from aea_ledger_ethereum.test_tools.constants import ETHEREUM_TESTNET_CONFIG from aea_ledger_fetchai import FetchAICrypto +from aea.cli.scaffold import add_contract_abi, scaffold_item +from aea.cli.utils.context import Context from aea.configurations.base import ComponentType, ContractConfig +from aea.configurations.constants import ( # noqa: F401 # pylint: disable=unused-import + CONTRACT, + CONTRACTS, +) from aea.configurations.loader import load_component_configuration from aea.contracts import contract_registry from aea.contracts.base import Contract @@ -200,6 +209,57 @@ def test_scaffold(): scaffold.get_state("ledger_api", "contract_address", **kwargs) +def test_scaffolded_contract_method_call(): + """Tests a contract method call.""" + + # Mock the CLI context + td = mkdtemp() + + @dataclass + class AgentConfig: + author = "dummy_author" + contracts = () + agent_name = "dummy_agent" + + ctx = Context(cwd=td, verbosity="DEBUG", registry_path=td) + ctx.agent_config = AgentConfig() + ctx.config["to_local_registry"] = True + ctx.agent_config.directory = td + + contract_name = "IUniswapV2ERC20" + contract_abi_path = Path("tests", "test_contracts", "IUniswapV2ERC20.json") + + try: + # Scaffold a new contract + scaffold_item(ctx, CONTRACT, contract_name) + add_contract_abi(ctx, contract_name, contract_abi_path) + + # Load the new contract + contract_path = Path(td, CONTRACTS, contract_name) + contract = Contract.from_dir(str(contract_path)) + ledger_api = ledger_apis_registry.make( + EthereumCrypto.identifier, + address=ETHEREUM_DEFAULT_ADDRESS, + ) + + # Call a contract method: allowance + SPENDER_ADDRESS = "0x7A1236d5195e31f1F573AD618b2b6FEFC85C5Ce6" + OWNER_ADDRESS = "0x7A1236d5195e31f1F573AD618b2b6FEFC85C5Ce6" + + with mock.patch("web3.contract.ContractFunction.call", return_value=0): + res = contract.contract_method_call( + ledger_api=ledger_api, + method_name="allowance", + owner=OWNER_ADDRESS, + spender=SPENDER_ADDRESS, + ) + + assert res == 0 + + finally: + shutil.rmtree(td) + + def test_contract_method_call(): """Tests a contract method call.""" contract = Contract.from_dir( @@ -248,6 +308,31 @@ def test_build_transaction_2(): assert result == {} +def test_default_method_call(): + """Tests a default method build.""" + dummy_address = "0x0000000000000000000000000000000000000000" + + contract = Contract.from_dir( + os.path.join(ROOT_DIR, "tests", "data", "dummy_contract") + ) + + ledger_api = ledger_apis_registry.make( + EthereumCrypto.identifier, + address=ETHEREUM_DEFAULT_ADDRESS, + ) + + # Call a function present in the ABI but not in the contract package + with mock.patch("web3.contract.ContractFunction.call", return_value=0): + result = contract.default_method_call( + ledger_api=ledger_api, + contract_address=dummy_address, + method_name="getAddress", + _addr=dummy_address, + ) + + assert result == 0 + + def test_get_transaction_transfer_logs(): """Tests a transaction log retrieval.""" contract = Contract.from_dir( diff --git a/tests/test_packages/test_connections/test_ledger/test_contract_api.py b/tests/test_packages/test_connections/test_ledger/test_contract_api.py index 5dd5a5c1c1..7973dd0c6e 100644 --- a/tests/test_packages/test_connections/test_ledger/test_contract_api.py +++ b/tests/test_packages/test_connections/test_ledger/test_contract_api.py @@ -20,6 +20,7 @@ """This module contains the tests of the ledger API connection for the contract APIs.""" import asyncio import logging +import os import re import unittest.mock from typing import cast @@ -31,6 +32,10 @@ from aea_ledger_ethereum.test_tools.constants import ETHEREUM_ADDRESS_ONE from aea.common import Address +from aea.contracts.base import Contract +from aea.crypto.ledger_apis import ETHEREUM_DEFAULT_ADDRESS +from aea.crypto.registries import ledger_apis_registry +from aea.exceptions import AEAException from aea.helpers.transaction.base import RawMessage, RawTransaction, State from aea.mail.base import Envelope from aea.multiplexer import MultiplexerStatus @@ -47,6 +52,8 @@ ) from packages.valory.protocols.contract_api.message import ContractApiMessage +from tests.conftest import ROOT_DIR + SOME_SKILL_ID = "some/skill:0.1.0" @@ -520,12 +527,9 @@ async def test_callable_cannot_find(erc1155_contract, ledger_apis_connection, ca message=request, ) - with mock.patch.object(ledger_apis_connection._logger, "debug") as mock_logger: - await ledger_apis_connection.send(envelope) - await asyncio.sleep(0.01) - assert ( - f"Cannot find {request.callable} in contract" in mock_logger.call_args[0][0] - ) + await ledger_apis_connection.send(envelope) + await asyncio.sleep(0.01) + assert f"Contract method {request.callable} not found" in caplog.text, caplog.text def test_build_response_fails_on_bad_data_type(): @@ -562,3 +566,41 @@ def test_build_response_fails_on_bad_data_type(): match=r"Invalid transaction type, got=, expected=typing.Dict", ): dispatcher.get_raw_transaction(MagicMock(), MagicMock(), MagicMock()) + + +def test_validate_and_call_callable(): + """Tests a default method call through ContractApiRequestDispatcher.""" + + dummy_address = "0x0000000000000000000000000000000000000000" + + contract = Contract.from_dir( + os.path.join(ROOT_DIR, "tests", "data", "dummy_contract") + ) + + ledger_api = ledger_apis_registry.make( + EthereumCrypto.identifier, + address=ETHEREUM_DEFAULT_ADDRESS, + ) + + message = MagicMock() + message.performative = ContractApiMessage.Performative.GET_STATE + message.kwargs.body = {"_addr": dummy_address} + message.callable = "getAddress" + message.contract_address = dummy_address + + # Call a method present in the ABI but not in the contract package + with mock.patch("web3.contract.ContractFunction.call", return_value=0): + result = ContractApiRequestDispatcher._validate_and_call_callable( + ledger_api, message, contract + ) + assert result == 0 + + # Call an non-existent method + message.callable = "dummy_method" + with pytest.raises( + AEAException, + match=f"Contract method dummy_method not found in ABI of contract {type(contract)}", + ): + ContractApiRequestDispatcher._validate_and_call_callable( + ledger_api, message, contract + )