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
+ )