Skip to content

Commit

Permalink
[FEAT-218] Allow Custom mint amount for RDN Tokens
Browse files Browse the repository at this point in the history
Implements feature as requested per #218 :

2 new keys are available in the `udc` section of the yaml file:

```yaml
settings:
  services:
    udc:
      deposit: true
      # The number of RDN/UDC Tokens required by each node
      balance_per_node: 5000
      # The maximum funding amount to deposit - defaults to `balance_per_node` if not given.
      max_funding: 5000
```
  • Loading branch information
Nils Diefenbach authored Aug 14, 2019
2 parents 3ddbdc4 + 3b62117 commit f3b93f3
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 22 deletions.
4 changes: 2 additions & 2 deletions scenario_player/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,10 @@ def _initialize_nodes(
self.node_controller.initialize_nodes()
node_addresses = self.node_controller.addresses
node_count = len(self.node_controller)
node_balances = {address: self.client.balance(address) for address in node_addresses}
balance_per_node = {address: self.client.balance(address) for address in node_addresses}
low_balances = {
address: balance
for address, balance in node_balances.items()
for address, balance in balance_per_node.items()
if balance < NODE_ACCOUNT_BALANCE_MIN
}
if low_balances:
Expand Down
30 changes: 26 additions & 4 deletions scenario_player/utils/configuration/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ def url(self):
return self.get("url")


class UDCTokenSettings(ConfigMapping):
def __init__(self, loaded_yaml: dict):
udc_settings = ((loaded_yaml.get("settings") or {}).get("services") or {}).get("udc") or {}
super(UDCTokenSettings, self).__init__(udc_settings.get("token"))
print(self.dict)

@property
def deposit(self):
return self.get("deposit", False)

@property
def balance_per_node(self):
"""The required amount of UDC/RDN tokens required by each node."""
return int(self.get("balance_per_node", 5000))

@property
def max_funding(self):
"""The maximum amount to fund when depositing RDN tokens at a target.
It defaults to :attr:`.balance_per_node`'s value.
"""
return int(self.get("max_funding", self.balance_per_node))


class UDCSettingsConfig(ConfigMapping):
"""UDC Service Settings interface.
Expand All @@ -54,13 +78,15 @@ class UDCSettingsConfig(ConfigMapping):
address: 0x1000001
token:
deposit: True
balance_per_node: 5000
...
"""

def __init__(self, loaded_yaml: dict):
services_dict = (loaded_yaml.get("settings") or {}).get("services") or {}
super(UDCSettingsConfig, self).__init__(services_dict.get("udc", {}))
self.validate()
self.token = UDCTokenSettings(loaded_yaml)

@property
def enable(self):
Expand All @@ -70,10 +96,6 @@ def enable(self):
def address(self):
return self.get("address")

@property
def token(self):
return self.get("token", {"deposit": False})


class ServiceSettingsConfig(ConfigMapping):
"""Service Configuration Setting interface.
Expand Down
15 changes: 10 additions & 5 deletions scenario_player/utils/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ def allowance(self):
self._local_rpc_client.address, self.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()

def mint(self, target_address) -> Union[str, None]:
"""The mint function isn't present on the UDC, pass the UDTC address instead."""
return super(UserDepositContract, self).mint(
Expand All @@ -362,7 +366,7 @@ def update_allowance(self) -> Union[str, None]:
"""
node_count = self.config.nodes.count
udt_allowance = self.allowance
required_allowance = self.config.token.min_balance * node_count
required_allowance = self.config.settings.services.udc.token.balance_per_node * node_count

log.debug(
"Checking necessity of deposit request",
Expand All @@ -375,7 +379,7 @@ def update_allowance(self) -> Union[str, None]:
return

log.debug("allowance update call required - insufficient allowance")
allow_amount = (self.config.token.max_funding * 10 * node_count) - udt_allowance
allow_amount = required_allowance - udt_allowance
params = {
"amount": allow_amount,
"target_address": self.checksum_address,
Expand All @@ -392,8 +396,9 @@ def deposit(self, target_address) -> Union[str, None]:
TODO: Allow setting max funding parameter, similar to the token `funding_min` setting.
"""
balance = self.contract_proxy.contract.functions.effectiveBalance(target_address).call()
min_deposit = self.config.token.min_balance // 2
balance = self.effective_balance(target_address)
min_deposit = self.config.settings.services.udc.token.balance_per_node
max_funding = self.config.settings.services.udc.token.max_funding
log.debug(
"Checking necessity of deposit request",
required_balance=min_deposit,
Expand All @@ -404,6 +409,6 @@ def deposit(self, target_address) -> Union[str, None]:
return

log.debug("deposit call required - insufficient funds")
deposit_amount = min_deposit - balance
deposit_amount = max_funding - balance
params = {"amount": deposit_amount, "target_address": target_address}
return self.transact("deposit", params)
2 changes: 1 addition & 1 deletion tests/unittests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def minimal_yaml_dict():
"""A dictionary with the minimum required keys for instantiating any ConfigMapping."""
return {
"scenario": {"serial": {"runner": None, "config": "salami"}},
"scenario": {"serial": {"tasks": {"wait_blocks": {"blocks": 5}}}},
"settings": {},
"token": {},
"nodes": {"count": 1},
Expand Down
9 changes: 9 additions & 0 deletions tests/unittests/data/minimal.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
scenario:
- serial:
- tasks: None
- wait_block: {"blocks": 5}
settings:
token:
nodes":
- count: 1
spaas:
41 changes: 34 additions & 7 deletions tests/unittests/utils/configuration/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ServiceSettingsConfig,
SettingsConfig,
UDCSettingsConfig,
UDCTokenSettings,
)


Expand Down Expand Up @@ -109,25 +110,51 @@ def test_is_subclass_of_config_mapping(self, minimal_yaml_dict):
"""The class is a subclass of :class:`ConfigMapping`."""
assert isinstance(UDCSettingsConfig(minimal_yaml_dict), ConfigMapping)

@pytest.mark.parametrize(
"key, expected",
argvalues=[("enable", False), ("address", None), ("token", {"deposit": False})],
)
def test_token_attribute_is_an_instance_of_udctokenconfig(self, minimal_yaml_dict):
assert isinstance(UDCSettingsConfig(minimal_yaml_dict).token, UDCTokenSettings)

@pytest.mark.parametrize("key, expected", argvalues=[("enable", False), ("address", None)])
def test_attributes_whose_key_is_absent_return_expected_default(
self, key, expected, minimal_yaml_dict
):
config = UDCSettingsConfig(minimal_yaml_dict)
MISSING = object()
assert getattr(config, key, MISSING) == expected

@pytest.mark.parametrize("key, expected", argvalues=[("enable", True), ("address", "walahoo")])
def test_attributes_return_for_key_value_if_key_present(
self, key, expected, minimal_yaml_dict
):
minimal_yaml_dict["settings"] = {"services": {"udc": {key: expected}}}
config = UDCSettingsConfig(minimal_yaml_dict)
MISSING = object()
assert getattr(config, key, MISSING) == expected


class TestUDCTokenConfig:
def test_is_subclass_of_config_mapping(self, minimal_yaml_dict):
"""The class is a subclass of :class:`ConfigMapping`."""
assert isinstance(UDCTokenSettings(minimal_yaml_dict), ConfigMapping)

@pytest.mark.parametrize(
"key, expected",
argvalues=[("enable", True), ("address", "walahoo"), ("token", {"deposit": True})],
argvalues=[("deposit", True), ("balance_per_node", 1000), ("max_funding", 10_000)],
)
def test_attributes_return_for_key_value_if_key_present(
self, key, expected, minimal_yaml_dict
):
minimal_yaml_dict["settings"] = {"services": {"udc": {key: expected}}}
config = UDCSettingsConfig(minimal_yaml_dict)
minimal_yaml_dict["settings"] = {"services": {"udc": {"token": {key: expected}}}}
config = UDCTokenSettings(minimal_yaml_dict)
MISSING = object()
assert getattr(config, key, MISSING) == expected

@pytest.mark.parametrize(
"key, expected",
argvalues=[("deposit", False), ("balance_per_node", 5000), ("max_funding", 5000)],
)
def test_attributes_whose_key_is_absent_return_expected_default(
self, key, expected, minimal_yaml_dict
):
config = UDCTokenSettings(minimal_yaml_dict)
MISSING = object()
assert getattr(config, key, MISSING) == expected
96 changes: 93 additions & 3 deletions tests/unittests/utils/test_token.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from unittest.mock import PropertyMock, patch
import pathlib
from unittest.mock import MagicMock, PropertyMock, patch

import pytest
from eth_utils.address import to_checksum_address
Expand All @@ -22,9 +23,10 @@
TokenNotDeployed,
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
from scenario_player.utils.token import Contract, Token, UserDepositContract

token_import_path = "scenario_player.utils.token"
token_config_import_path = "scenario_player.utils.configuration.token"
Expand All @@ -37,7 +39,10 @@ class Sentinel(Exception):
@pytest.fixture
def runner(dummy_scenario_runner, minimal_yaml_dict, token_info_path, tmp_path):
token_config = TokenConfig(minimal_yaml_dict, token_info_path)
dummy_scenario_runner.yaml.spaas = SPaaSConfig(minimal_yaml_dict)
with patch("yaml.safe_load", return_value=minimal_yaml_dict):
tmp_file = tmp_path.joinpath("tmp.yaml")
tmp_file.touch()
dummy_scenario_runner.yaml = ScenarioYAML(tmp_file, tmp_path)
dummy_scenario_runner.yaml.spaas.rpc.client_id = "the_client_id"
dummy_scenario_runner.yaml.token = token_config

Expand Down Expand Up @@ -468,3 +473,88 @@ def json(self):
if reuse_token:
return
pytest.fail(f"save_token called, but reuse_token is {reuse_token}")


class TestUserDepositContract:
# TODO: Write tests to assert correctness of allowance and effective_balance properties
@pytest.fixture(autouse=True)
def set_up_udc_test_class(self, runner):
with patch(
"scenario_player.utils.token.UserDepositContract.effective_balance"
) as mock_eb, patch(
"scenario_player.utils.token.UserDepositContract.allowance", new_callable=PropertyMock
) as mock_allowance, patch(
"scenario_player.utils.token.UserDepositContract.ud_token_address",
new_callable=PropertyMock,
) as mock_ud_token_addr:
# We'll mock the properties accessing the contract proxies for now
# TODO: Properly mock them instead.
self.instance = UserDepositContract(runner, MagicMock(), MagicMock())
self.mock_allowance = mock_allowance
self.mock_effective_balance = mock_eb
self.mock_ud_token_address = mock_ud_token_addr
yield

@patch("scenario_player.utils.token.Contract.transact")
def test_update_allowance_is_noop_if_allowance_is_sufficient(self, mock_transact):
self.instance.config.nodes.dict["count"] = 2
self.instance.config.settings.services.udc.token.dict["balance_per_node"] = 5_000
self.mock_allowance.return_value = 10_000

self.instance.update_allowance()

assert mock_transact.called is False

@patch(
"scenario_player.utils.token.Contract.checksum_address",
new_callable=PropertyMock(return_value="ud_contract_addr"),
)
@patch("scenario_player.utils.token.Contract.transact")
def test_update_allowance_updates_allowance_according_to_udc_token_balance_per_node_yaml_setting(
self, mock_transact, _
):
self.instance.config.nodes.dict["count"] = 2
self.instance.config.settings.services.udc.token.dict["balance_per_node"] = 15_000
self.mock_allowance.return_value = 5_000
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
# Targeting the UD Contract address, calling from the UD Token address.
expected_params = {
"amount": 25_000,
"target_address": "ud_contract_addr",
"contract_address": "ud_token_addr",
}

self.instance.update_allowance()

mock_transact.assert_called_once_with("allowance", expected_params)

@patch("scenario_player.utils.token.Contract.transact")
def test_deposit_method_issues_deposit_request_if_node_funding_is_insufficient(
self, mock_transact
):
"""deposit() deposits the correct amount of UDC Tokens at the target node's address.
amount = yaml.settings.services.udc.token.max_funding - target_node.effective_balance
"""
self.instance.config.settings.services.udc.token.dict["balance_per_node"] = 5_000
self.instance.config.settings.services.udc.token.dict["max_funding"] = 10_000
self.mock_effective_balance.return_value = 1000

# amount = max_funding - effective_balance
expected_params = {"amount": 9_000, "target_address": "some_address"}

self.instance.deposit("some_address")

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):
self.instance.config.settings.services.udc.token.dict["balance_per_node"] = 5_000
self.mock_effective_balance.return_value = 10_000

self.instance.deposit("some_address")

assert mock_transact.called is False

0 comments on commit f3b93f3

Please sign in to comment.