diff --git a/scenario_player/runner.py b/scenario_player/runner.py index 53054801b..94458c332 100644 --- a/scenario_player/runner.py +++ b/scenario_player/runner.py @@ -308,12 +308,16 @@ def _initialize_udc( self.udc = UserDepositContract(self, udc_ctr, ud_token_ctr) should_deposit_ud_token = udc_enabled and udc_settings.token["deposit"] - allowance_tx = self.udc.update_allowance() + allowance_tx, required_allowance = self.udc.update_allowance() if allowance_tx: ud_token_tx.add(allowance_tx) if should_deposit_ud_token: - tx = self.udc.mint(our_address) + tx = self.udc.mint( + our_address, + required_balance=required_allowance, + max_fund_amount=required_allowance * 2, + ) if tx: ud_token_tx.add(tx) @@ -333,6 +337,7 @@ def _initialize_nodes( for address, balance in balance_per_node.items() if balance < NODE_ACCOUNT_BALANCE_MIN } + log.debug("Node eth balances", balances=balance_per_node, low_balances=low_balances) if low_balances: log.info("Funding nodes", nodes=low_balances.keys()) fund_tx = set() diff --git a/scenario_player/scenario.py b/scenario_player/scenario.py index 10567ce30..8c92d5342 100644 --- a/scenario_player/scenario.py +++ b/scenario_player/scenario.py @@ -6,7 +6,6 @@ import yaml from scenario_player.constants import GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL -from scenario_player.exceptions.config import InsufficientMintingAmount from scenario_player.utils.configuration import ( NodesConfig, ScenarioConfig, @@ -34,7 +33,6 @@ def __init__(self, yaml_path: pathlib.Path, data_path: pathlib.Path) -> None: self.scenario = ScenarioConfig(self._loaded) self.token = TokenConfig(self._loaded, data_path.joinpath("token.info")) self.spaas = SPaaSConfig(self._loaded) - self.validate() self.gas_limit = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2 @@ -42,16 +40,3 @@ def __init__(self, yaml_path: pathlib.Path, data_path: pathlib.Path) -> None: def name(self) -> str: """Return the name of the scenario file, sans extension.""" return self.path.stem - - def validate(self): - """Validate cross-config section requirements of the scenario. - - :raises InsufficientMintingAmount: - If token.min_balance < settings.services.udc.token.max_funding - """ - - # Check that the amount of minted tokens is >= than the amount of deposited tokens - try: - assert self.token.min_balance >= self.settings.services.udc.token.max_funding - except AssertionError: - raise InsufficientMintingAmount diff --git a/scenario_player/services/rpc/blueprints/tokens.py b/scenario_player/services/rpc/blueprints/tokens.py index c5b242c75..6a0dcdc15 100644 --- a/scenario_player/services/rpc/blueprints/tokens.py +++ b/scenario_player/services/rpc/blueprints/tokens.py @@ -208,7 +208,7 @@ def transact_call(key, data): contract_proxy = rpc_client.new_contract_proxy(contract_abi, data["contract_address"]) - log.debug("Transacting..", **data) + log.debug("Transacting..", action=action, **data) args = data["amount"], data["target_address"] if action != "mintFor": diff --git a/scenario_player/services/rpc/blueprints/transactions.py b/scenario_player/services/rpc/blueprints/transactions.py index c2594f522..45793e84d 100644 --- a/scenario_player/services/rpc/blueprints/transactions.py +++ b/scenario_player/services/rpc/blueprints/transactions.py @@ -12,10 +12,14 @@ """ from flask import Blueprint, request +from structlog import get_logger from scenario_player.services.common.metrics import REDMetricsTracker from scenario_player.services.rpc.schemas.transactions import SendTransactionSchema +log = get_logger(__name__) + + transactions_blueprint = Blueprint("transactions_view", __name__) @@ -77,6 +81,7 @@ def new_transaction(): data = transaction_send_schema.validate_and_deserialize(request.get_json()) rpc_client, _ = data.pop("client"), data.pop("client_id") + log.debug("Performing transaction", params=data) result = rpc_client.send_transaction(**data) return transaction_send_schema.jsonify({"tx_hash": result}) diff --git a/scenario_player/utils/token.py b/scenario_player/utils/token.py index 979b7983d..474f9326a 100644 --- a/scenario_player/utils/token.py +++ b/scenario_player/utils/token.py @@ -27,6 +27,13 @@ def __init__(self, runner, address=None): self.interface = ServiceInterface(runner.yaml.spaas) self.gas_limit = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2 + def __repr__(self): + return f"<{self.name}>" + + @property + def name(self): + return f"{self.__class__.__name__}@{to_checksum_address(self.address)}" + @property def client_id(self): return self.config.spaas.rpc.client_id @@ -59,28 +66,35 @@ def transact(self, action: str, parameters: dict) -> str: resp_data = resp.json() tx_hash = resp_data["tx_hash"] log.info(f"'{action}' call succeeded", tx_hash=tx_hash) - return decode_hex(tx_hash) + return decode_hex(tx_hash).decode() - def mint(self, target_address, **kwargs) -> Union[str, None]: + def mint( + self, target_address, required_balance=None, max_fund_amount=None, **kwargs + ) -> Union[str, None]: """Mint new tokens for the given `target_address`. The amount of tokens depends on the scenario yaml's settings, and defaults to :attr:`.DEFAULT_TOKEN_BALANCE_MIN` and :attr:`.DEFAULT_TOKEN_BALANCE_FUND` if those settings are absent. """ + local_log = log.bind(contract=self.name) balance = self.balance - required_balance = self.config.token.min_balance - log.debug( + if required_balance is None: + required_balance = self.config.token.min_balance + local_log.debug( "Checking necessity of mint request", required_balance=required_balance, actual_balance=balance, ) if not balance < required_balance: - log.debug("Mint call not required - sufficient funds") + local_log.debug("Mint call not required - sufficient funds") return - mint_amount = self.config.token.max_funding - balance - log.debug("Minting required - insufficient funds.") + if max_fund_amount is None: + max_fund_amount = self.config.token.max_funding + + mint_amount = max_fund_amount - balance + local_log.debug("Minting required - insufficient funds.", mint_amount=mint_amount) params = {"amount": mint_amount, "target_address": target_address} params.update(kwargs) return self.transact("mint", params) @@ -98,10 +112,11 @@ class Token(Contract): """ def __init__(self, scenario_runner, data_path: pathlib.Path): - super(Token, self).__init__(scenario_runner) + super().__init__(scenario_runner) self._token_file = data_path.joinpath("token.info") self.contract_data = {} self.deployment_receipt = None + self.contract_proxy = None @property def name(self) -> str: @@ -159,7 +174,7 @@ def balance(self) -> float: It is an error to access this property before the token is deployed. """ if self.deployed: - return super(Token, self).balance + return self.contract_proxy.contract.functions.balanceOf(self.address).call() else: raise TokenNotDeployed @@ -258,18 +273,24 @@ def use_existing(self) -> Tuple[str, int]: f"Cannot reuse token - address {address} has no code stored!" ) from e - # Fetch the token's contract_proxy data. - contract_proxy = self._local_contract_manager.get_contract(contract_name) + # Fetch the token's contract_info data. + contract_info = self._local_contract_manager.get_contract(contract_name) - self.contract_data = {"token_contract": address, "name": contract_proxy.name} + self.contract_data = { + "token_contract": address, + "name": contract_info.get("name") or contract_name, + } + self.contract_proxy = self._local_rpc_client.new_contract_proxy( + contract_info["abi"], address + ) self.deployment_receipt = {"blockNum": block} checksummed_address = to_checksum_address(address) log.debug( "Reusing token", address=checksummed_address, - name=contract_proxy.name, - symbol=contract_proxy.symbol, + name=contract_name, + symbol=self.contract_proxy.contract.functions.symbol().call(), ) return checksummed_address, block @@ -307,9 +328,15 @@ def deploy_new(self) -> Tuple[str, int]: resp_data["contract"], resp_data["deployment_block"], ) + print(token_contract_data) + print(deployment_block) + contract_info = self._local_contract_manager.get_contract("CustomToken") # Make deployment address and block available to address/deployment_block properties. self.contract_data = token_contract_data + self.contract_proxy = self._local_rpc_client.new_contract_proxy( + contract_info["abi"], token_contract_data["address"] + ) self.deployment_receipt = {"blockNumber": deployment_block} if self.config.token.reuse_token: @@ -331,9 +358,7 @@ class UserDepositContract(Contract): """ def __init__(self, scenario_runner, contract_proxy, token_proxy): - super(UserDepositContract, self).__init__( - scenario_runner, address=contract_proxy.contract_address - ) + super().__init__(scenario_runner, address=contract_proxy.contract_address) self.contract_proxy = contract_proxy self.token_proxy = token_proxy self.tx_hashes = set() @@ -349,6 +374,11 @@ def allowance(self): self._local_rpc_client.address, self.address ).call() + @property + def balance(self): + """Proxy the balance call to the UDTC.""" + return self.token_proxy.contract.functions.balanceOf(self.ud_token_address).call() + def effective_balance(self, at_target): """Get the effective balance of the target address.""" return self.contract_proxy.contract.functions.effectiveBalance(at_target).call() @@ -357,13 +387,19 @@ def total_deposit(self, at_target): """"Get the so far deposted amount""" return self.contract_proxy.contract.functions.total_deposit(at_target).call() - def mint(self, target_address) -> Union[str, None]: + def mint( + self, target_address, required_balance=None, max_fund_amount=None, **kwargs + ) -> Union[str, None]: """The mint function isn't present on the UDC, pass the UDTC address instead.""" - return super(UserDepositContract, self).mint( - target_address, contract_address=self.ud_token_address + return super().mint( + target_address, + required_balance=required_balance, + max_fund_amount=max_fund_amount, + contract_address=self.ud_token_address, + **kwargs, ) - def update_allowance(self) -> Union[str, None]: + def update_allowance(self) -> Union[Tuple[str, int], None]: """Update the UD Token Contract allowance depending on the number of configured nodes. If the UD Token Contract's allowance is sufficient, this is a no-op. @@ -373,23 +409,24 @@ def update_allowance(self) -> Union[str, None]: required_allowance = self.config.settings.services.udc.token.balance_per_node * node_count log.debug( - "Checking necessity of deposit request", - required_balance=required_allowance, - actual_balance=udt_allowance, + "Checking UDTC allowance", + required_allowance=required_allowance, + required_per_node=self.config.settings.services.udc.token.balance_per_node, + node_count=node_count, + actual_allowance=udt_allowance, ) if not udt_allowance < required_allowance: - log.debug("allowance update call not required - sufficient allowance") + log.debug("UDTC allowance sufficient") return - log.debug("allowance update call required - insufficient allowance") - allow_amount = required_allowance - udt_allowance + log.debug("UDTC allowance insufficient, updating") params = { - "amount": allow_amount, + "amount": required_allowance, "target_address": self.checksum_address, "contract_address": self.ud_token_address, } - return self.transact("allowance", params) + return self.transact("allowance", params), required_allowance def deposit(self, target_address) -> Union[str, None]: """Make a deposit at the given `target_address`. @@ -406,6 +443,7 @@ def deposit(self, target_address) -> Union[str, None]: max_funding = self.config.settings.services.udc.token.max_funding log.debug( "Checking necessity of deposit request", + target_address=target_address, required_balance=min_deposit, actual_balance=balance, ) @@ -413,7 +451,7 @@ def deposit(self, target_address) -> Union[str, None]: log.debug("deposit call not required - sufficient funds") return - log.debug("deposit call required - insufficient funds") + log.debug("deposit call required - insufficient funds", target_address=target_address) deposit_amount = total_deposit + (max_funding - balance) params = {"amount": deposit_amount, "target_address": target_address} return self.transact("deposit", params) diff --git a/tests/unittests/utils/configuration/test_settings.py b/tests/unittests/utils/configuration/test_settings.py index a2e07fdbf..f85a90950 100644 --- a/tests/unittests/utils/configuration/test_settings.py +++ b/tests/unittests/utils/configuration/test_settings.py @@ -176,9 +176,3 @@ def test_balance_per_node_must_not_be_greater_than_max_funding(self, minimal_yam } with pytest.raises(UDCTokenConfigError): UDCTokenSettings(minimal_yaml_dict) - - def test_insufficient_minting(self, file_for_insufficient_minting_test): - with pytest.raises(InsufficientMintingAmount): - ScenarioYAML( - file_for_insufficient_minting_test, file_for_insufficient_minting_test.parent - ) diff --git a/tests/unittests/utils/test_token.py b/tests/unittests/utils/test_token.py index d4e8ebade..0a54a8ce1 100644 --- a/tests/unittests/utils/test_token.py +++ b/tests/unittests/utils/test_token.py @@ -1,9 +1,9 @@ import json -import pathlib + from unittest.mock import MagicMock, PropertyMock, patch import pytest -from eth_utils.address import to_checksum_address +from eth_utils.address import to_checksum_address, to_hex from requests.exceptions import ( ConnectionError, ConnectTimeout, @@ -24,7 +24,6 @@ TokenSourceCodeDoesNotExist, ) from scenario_player.scenario import ScenarioYAML -from scenario_player.utils.configuration.spaas import SPaaSConfig from scenario_player.utils.configuration.token import TokenConfig from scenario_player.utils.token import Contract, Token, UserDepositContract @@ -32,10 +31,22 @@ token_config_import_path = "scenario_player.utils.configuration.token" +# TODO: These tests mock and use dummy values even though it isn't necessary; +# For example, using "my_address" instead of a proper hex address. + + class Sentinel(Exception): """Raised when it's desired to exit a method under test early.""" +@pytest.fixture +def hex_address(): + return "0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c" + +@pytest.fixture +def contract_addr(): + return "0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c" + @pytest.fixture def runner(dummy_scenario_runner, minimal_yaml_dict, token_info_path, tmp_path): token_config = TokenConfig(minimal_yaml_dict, token_info_path) @@ -57,8 +68,8 @@ def token_instance(runner, tmp_path): @pytest.fixture -def contract_instance(runner, tmp_path): - return Contract(runner, "my_address") +def contract_instance(runner, tmp_path, contract_addr): + return Contract(runner, contract_addr) class TestContract: @@ -111,9 +122,9 @@ def setup_instance_with_balance(instance, current_balance): return instance @patch(f"{token_import_path}.ServiceInterface.request") - def test_mint_is_a_no_op_if_balance_is_sufficient(self, mock_request, contract_instance): + def test_mint_is_a_no_op_if_balance_is_sufficient(self, mock_request, contract_instance, hex_address): contract_instance = self.setup_instance_with_balance(contract_instance, 100000) - assert contract_instance.mint("the_address") is None + assert contract_instance.mint(hex_address) is None assert mock_request.called is False @patch(f"{token_import_path}.ServiceInterface.post", side_effect=Sentinel) @@ -374,7 +385,7 @@ class MockContractProxy: name = "my_deployed_token_name" symbol = "token_symbol" - token_instance._local_contract_manager.get_contract.return_value = MockContractProxy + token_instance._local_contract_manager.get_contract.return_value = {"abi": "contract_abi", "name": "my_deployed_token_name"} expected_deployment_receipt = {"blockNum": loaded_token_info["block"]} expected_contract_data = { @@ -455,7 +466,7 @@ def json(self): def test_deploy_new_calls_save_token_depending_on_reuse_token_property( self, _, mock_save_token, mock_request, reuse_token, token_instance ): - json_resp = {"contract": {}, "deployment_block": 1} + json_resp = {"contract": {"address": None}, "deployment_block": 1} class MockResp: def json(self): @@ -519,10 +530,10 @@ def test_update_allowance_updates_allowance_according_to_udc_token_balance_per_n self.mock_ud_token_address.return_value = "ud_token_addr" # Expect an allowance transact request to be invoked, with its amount equal to: - # (balance_per_node * node_count) - current_allowance + # balance_per_node * node_count # Targeting the UD Contract address, calling from the UD Token address. expected_params = { - "amount": 25_000, + "amount": 30_000, "target_address": "ud_contract_addr", "contract_address": "ud_token_addr", } @@ -554,7 +565,7 @@ def test_deposit_method_issues_deposit_request_if_node_funding_is_insufficient( mock_transact.assert_called_once_with("deposit", expected_params) @patch("scenario_player.utils.token.Contract.transact") - def test_deposit_methpd_is_noop_if_node_funding_is_sufficient(self, mock_transact): + def test_deposit_method_is_noop_if_node_funding_is_sufficient(self, mock_transact): self.instance.config.settings.services.udc.token.dict["balance_per_node"] = 5_000 self.mock_effective_balance.return_value = 10_000