Skip to content

Commit

Permalink
Add unit and integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
F4ever committed Aug 15, 2023
1 parent c2cd7c6 commit d59fc4a
Show file tree
Hide file tree
Showing 43 changed files with 669 additions and 868 deletions.
4 changes: 2 additions & 2 deletions src/blockchain/contracts/deposit_security_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ def get_pause_message_prefix(self, block_identifier: BlockIdentifier = 'latest')
logger.info({'msg': f'Call `PAUSE_MESSAGE_PREFIX()`.', 'value': response.hex(), 'block_identifier': block_identifier.__repr__()})
return response

def get_pause_intent_validity_period_blocks(self, block_identifier: BlockIdentifier = 'latest') -> bytes:
def get_pause_intent_validity_period_blocks(self, block_identifier: BlockIdentifier = 'latest') -> int:
"""Returns current `pauseIntentValidityPeriodBlocks` contract parameter (see `pauseDeposits`)."""
response = self.functions.getPauseIntentValidityPeriodBlocks().call(block_identifier=block_identifier)
logger.info({'msg': f'Call `getPauseIntentValidityPeriodBlocks()`.', 'value': response.hex(), 'block_identifier': block_identifier.__repr__()})
logger.info({'msg': f'Call `getPauseIntentValidityPeriodBlocks()`.', 'value': response, 'block_identifier': block_identifier.__repr__()})
return response

def pause_deposits(
Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/contracts/staking_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def get_max_deposits_count(
})
return response

def get_staking_module_ids(self, block_identifier: BlockIdentifier = 'latest'):
def get_staking_module_ids(self, block_identifier: BlockIdentifier = 'latest') -> list[int]:
"""Returns the ids of all registered staking modules"""
response = self.functions.getStakingModuleIds().call(block_identifier=block_identifier)
logger.info({
Expand Down
30 changes: 22 additions & 8 deletions src/blockchain/deposit_strategy/curated_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy
from eth_typing import BlockNumber
from web3 import Web3
from web3.types import Wei

import variables
from blockchain.deposit_strategy.interface import ModuleDepositStrategyInterface
Expand All @@ -29,6 +30,13 @@ def __init__(self, w3: Web3, module_id: int):
self._days_param = None

def is_deposited_keys_amount_ok(self) -> bool:
possible_deposits_amount = self._get_possible_deposits_amount()
recommended_max_gas = self._calculate_recommended_gas_based_on_deposit_amount(possible_deposits_amount)

base_fee_per_gas = self._get_pending_base_fee()
return recommended_max_gas >= base_fee_per_gas

def _get_possible_deposits_amount(self) -> int:
depositable_ether = self.w3.lido.lido.get_depositable_ether()
DEPOSITABLE_ETHER.labels(self.module_id).set(depositable_ether)

Expand All @@ -37,34 +45,40 @@ def is_deposited_keys_amount_ok(self) -> bool:
depositable_ether,
)
POSSIBLE_DEPOSITS_AMOUNT.labels(self.module_id).set(possible_deposits_amount)
return possible_deposits_amount

def _calculate_recommended_gas_based_on_deposit_amount(self, deposits_amount: int) -> Wei:
# For one key recommended gas fee will be around 10
# For 10 keys around 100 gwei. For 20 keys ~ 800 gwei
recommended_max_gas = (possible_deposits_amount**3 + 100) * 10**8
recommended_max_gas = (deposits_amount ** 3 + 100) * 10 ** 8
logger.info({'msg': 'Calculate recommended max gas based on possible deposits.'})
GAS_FEE.labels('based_on_buffer_fee', self.module_id).set(recommended_max_gas)
return recommended_max_gas

def _get_pending_base_fee(self):
base_fee_per_gas = self.w3.eth.get_block('pending')['baseFeePerGas']
logger.info({'msg': 'Fetch base_fee_per_gas for pending block.', 'value': base_fee_per_gas})

return recommended_max_gas >= base_fee_per_gas
return base_fee_per_gas

def is_gas_price_ok(self) -> bool:
gas_history = self._fetch_gas_fee_history(variables.GAS_FEE_PERCENTILE_DAYS_HISTORY_1)
recommended_gas_fee = int(numpy.percentile(gas_history, variables.GAS_FEE_PERCENTILE_1))
current_gas_fee = self._get_pending_base_fee()
GAS_FEE.labels('current_fee', self.module_id).set(current_gas_fee)

current_gas_fee = self.w3.eth.get_block('pending')['baseFeePerGas']
recommended_gas_fee = self._get_recommended_gas_fee()
GAS_FEE.labels('recommended_fee', self.module_id).set(recommended_gas_fee)

GAS_FEE.labels('max_fee', self.module_id).set(variables.MAX_GAS_FEE)
GAS_FEE.labels('current_fee', self.module_id).set(current_gas_fee)
GAS_FEE.labels('recommended_fee', self.module_id).set(recommended_gas_fee)

current_buffered_ether = self.w3.lido.lido.get_depositable_ether()
if current_buffered_ether > variables.MAX_BUFFERED_ETHERS:
return variables.MAX_GAS_FEE >= current_gas_fee

return recommended_gas_fee >= current_gas_fee

def _get_recommended_gas_fee(self):
gas_history = self._fetch_gas_fee_history(variables.GAS_FEE_PERCENTILE_DAYS_HISTORY_1)
return int(numpy.percentile(gas_history, variables.GAS_FEE_PERCENTILE_1))

def _fetch_gas_fee_history(self, days: int) -> list[int]:
latest_block_num = self.w3.eth.get_block('latest')['number']

Expand Down
9 changes: 6 additions & 3 deletions src/blockchain/executer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from blockchain.constants import SLOT_TIME
from blockchain.typings import Web3
from metrics.healthcheck_pulse import pulse
from metrics import healthcheck_pulse
from utils.timeout import TimeoutManager, TimeoutManagerError


Expand Down Expand Up @@ -42,8 +42,8 @@ def execute_as_daemon(self) -> None:
while True:
self._wait_for_new_block_and_execute()

def _wait_for_new_block_and_execute(self):
pulse()
def _wait_for_new_block_and_execute(self) -> Any:
healthcheck_pulse.pulse()

latest_block = self._exception_handler(self._wait_until_next_block)
result = self._exception_handler(self._execute_function, latest_block)
Expand All @@ -54,6 +54,8 @@ def _wait_for_new_block_and_execute(self):
# If function do not return success code (True or whatever) retry function call with next block.
self._next_expected_block += 1

return result

def _wait_until_next_block(self) -> BlockData:
with TimeoutManager(max(
# Wait at least 5 slots before throw exception
Expand All @@ -65,6 +67,7 @@ def _wait_until_next_block(self) -> BlockData:
logger.debug({'msg': 'Fetch latest block.', 'value': latest_block})

if latest_block['number'] >= self._next_expected_block:
self._next_expected_block = latest_block['number']
return latest_block

time_until_expected_block = (self._next_expected_block - latest_block.number - 1) * SLOT_TIME
Expand Down
4 changes: 2 additions & 2 deletions src/blockchain/web3_extentions/lido_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ def __init__(self, w3: Web3):

def _load_contracts(self):
self.deposit_contract: DepositContract = cast(DepositContract, self.web3.eth.contract(
address=DEPOSIT_CONTRACT[WEB3_CHAIN_ID],
address=DEPOSIT_CONTRACT[self.web3.eth.chain_id],
ContractFactoryClass=DepositContract,
))

self.lido_locator: LidoLocatorContract = cast(LidoLocatorContract, self.web3.eth.contract(
# ToDo provide lido locator address via env variable
address=LIDO_LOCATOR[WEB3_CHAIN_ID],
address=LIDO_LOCATOR[self.web3.eth.chain_id],
ContractFactoryClass=LidoLocatorContract,
))

Expand Down
4 changes: 2 additions & 2 deletions src/bots/depositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def execute(self, block: BlockData) -> bool:
# Return failed status to start new cycle with next block, until no deposits done
return False

return True
return True

def _check_balance(self):
if variables.ACCOUNT:
Expand All @@ -99,7 +99,7 @@ def _check_balance(self):
'value': balance,
})
else:
logger.info({'msg': 'Check account balance. Dry mode.'})
logger.info({'msg': 'No account provided. Dry mode.'})
ACCOUNT_BALANCE.set(0)

def _deposit_to_module(self, module_id: int) -> bool:
Expand Down
1 change: 0 additions & 1 deletion src/bots/pause.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def __init__(self, w3: Web3):

if not transports:
logger.warning({'msg': 'No transports found', 'value': variables.MESSAGE_TRANSPORTS})
raise ValueError(f'No transports found. Provided value: {variables.MESSAGE_TRANSPORTS}')

pause_prefix = self.w3.lido.deposit_security_module.get_

Expand Down
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions tests/blockchain/contracts/test_deposit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pytest

from tests.utils.contract_utils import check_contract
from tests.utils.regrex import HASH_REGREX, check_value_re


@pytest.mark.integration
def test_deposit_contract_call(deposit_contract, caplog):
check_contract(
deposit_contract,
[
('get_deposit_root', None, lambda response: check_value_re(HASH_REGREX, '0x' + response.hex())),
],
caplog,
)
21 changes: 21 additions & 0 deletions tests/blockchain/contracts/test_deposit_security_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from tests.utils.contract_utils import check_contract
from tests.utils.regrex import HASH_REGREX, check_value_re, check_value_type, ADDRESS_REGREX


@pytest.mark.integration
def test_deposit_security_module_call(deposit_security_module, caplog):
check_contract(
deposit_security_module,
[
('get_guardian_quorum', None, lambda response: check_value_type(response, int)),
('get_guardians', None, lambda response: check_value_type(response, list) and
[check_value_re(ADDRESS_REGREX, g) for g in response]),
('get_attest_message_prefix', None, lambda response: check_value_type(response, bytes)),
('can_deposit', (1,), lambda response: check_value_type(response, bool)),
('get_pause_message_prefix', None, lambda response: check_value_type(response, bytes)),
('get_pause_intent_validity_period_blocks', None, lambda response: check_value_type(response, int)),
],
caplog,
)
12 changes: 12 additions & 0 deletions tests/blockchain/contracts/test_lido.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tests.utils.contract_utils import check_contract
from tests.utils.regrex import check_value_type


def test_lido_contract_call(lido_contract, caplog):
check_contract(
lido_contract,
[
('get_depositable_ether', None, lambda response: check_value_type(response, int)),
],
caplog,
)
14 changes: 14 additions & 0 deletions tests/blockchain/contracts/test_lido_locator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from tests.utils.contract_utils import check_contract
from tests.utils.regrex import check_value_re, ADDRESS_REGREX


def test_lido_locator_call(lido_locator, caplog):
check_contract(
lido_locator,
[
('lido', None, lambda response: check_value_re(ADDRESS_REGREX, response)),
('deposit_security_module', None, lambda response: check_value_re(ADDRESS_REGREX, response)),
('staking_router', None, lambda response: check_value_re(ADDRESS_REGREX, response)),
],
caplog,
)
21 changes: 21 additions & 0 deletions tests/blockchain/contracts/test_staking_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from tests.utils.contract_utils import check_contract
from tests.utils.regrex import check_value_type


@pytest.mark.integration
def test_staking_router_call(staking_router, caplog):
check_contract(
staking_router,
[
('get_max_deposits_count', (1, 100*10**18), lambda response: check_value_type(response, int)),
('get_staking_module_ids', None, lambda response: check_value_type(response, list) and
[check_value_type(x, int) for x in response]),
('is_staking_module_active', (1,), lambda response: check_value_type(response, bool)),
('is_staking_module_deposits_paused', (1,), lambda response: check_value_type(response, bool)),
('get_staking_module_nonce', (1,), lambda response: check_value_type(response, int)),
('get_staking_module_deposits_count', (1, 100*10**18), lambda response: check_value_type(response, int)),
],
caplog,
)
Empty file.
108 changes: 108 additions & 0 deletions tests/blockchain/deposit_strategy/test_curated_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from unittest.mock import Mock

import pytest

import variables
from blockchain.deposit_strategy.curated_module import CuratedModuleDepositStrategy


MODULE_ID = 1337


@pytest.fixture
def cmds(web3_lido_unit):
yield CuratedModuleDepositStrategy(web3_lido_unit, module_id=MODULE_ID)


@pytest.mark.unit
def test_is_deposited_keys_amount_ok(cmds):
cmds._get_possible_deposits_amount = Mock(return_value=100)

cmds._calculate_recommended_gas_based_on_deposit_amount = Mock(return_value=30)
cmds._get_pending_base_fee = Mock(return_value=20)

assert cmds.is_deposited_keys_amount_ok()

cmds._get_pending_base_fee = Mock(return_value=50)
assert not cmds.is_deposited_keys_amount_ok()


@pytest.mark.unit
def test_get_possible_deposits_amount(cmds):
depositable_eth = 100
possible_deposits = depositable_eth // 32

cmds.w3.lido.lido.get_depositable_ether = Mock(return_value=depositable_eth)
cmds.w3.lido.staking_router.get_staking_module_deposits_count = Mock(return_value=possible_deposits)

assert cmds._get_possible_deposits_amount() == possible_deposits
cmds.w3.lido.staking_router.get_staking_module_deposits_count.assert_called_once_with(
MODULE_ID,
depositable_eth,
)


@pytest.mark.unit
@pytest.mark.parametrize(
"deposits,expected_range",
[(1, (0, 20)), (5, (20, 100)), (10, (50, 1000)), (100, (1000, 1000000))],
)
def test_calculate_recommended_gas_based_on_deposit_amount(cmds, deposits, expected_range):
assert expected_range[0] * 10**9 <= cmds._calculate_recommended_gas_based_on_deposit_amount(deposits) <= expected_range[1] * 10**9


@pytest.mark.unit
def test_get_recommended_gas_fee(cmds):
cmds._fetch_gas_fee_history = Mock(return_value=list(range(11)))
variables.GAS_FEE_PERCENTILE_DAYS_HISTORY_1 = 1
variables.GAS_FEE_PERCENTILE_1 = 50

assert cmds._get_recommended_gas_fee() == 5

variables.GAS_FEE_PERCENTILE_1 = 30
assert cmds._get_recommended_gas_fee() == 3


@pytest.mark.unit
def test_is_gas_price_ok(cmds):
cmds._get_pending_base_fee = Mock(return_value=10)
cmds._get_recommended_gas_fee = Mock(return_value=20)
variables.MAX_GAS_FEE = 300

cmds.w3.lido.lido.get_depositable_ether = Mock(return_value=100)
variables.MAX_BUFFERED_ETHERS = 200
assert cmds.is_gas_price_ok()

cmds._get_recommended_gas_fee = Mock(return_value=5)
assert not cmds.is_gas_price_ok()

cmds.w3.lido.lido.get_depositable_ether = Mock(return_value=300)
assert cmds.is_gas_price_ok()

cmds._get_pending_base_fee = Mock(return_value=400)
assert not cmds.is_gas_price_ok()


@pytest.fixture()
def cmds_integration(web3_lido_integration):
yield CuratedModuleDepositStrategy(web3_lido_integration, module_id=MODULE_ID)


@pytest.mark.integration
def test_get_pending_base_fee(cmds_integration):
pending_gas = cmds_integration._get_pending_base_fee()
assert 1 <= pending_gas <= 1000 * 10**9


@pytest.mark.integration
def test_fetch_gas_fee_history(cmds_integration):
history = cmds_integration._fetch_gas_fee_history(1)
assert isinstance(history, list)
assert len(history) == 1 * 24 * 60 * 60 / 12

cmds_integration.w3.eth.fee_history = Mock()
cmds_integration._fetch_gas_fee_history(1)
assert len(history) == 1 * 24 * 60 * 60 / 12
cmds_integration.w3.eth.fee_history.assert_not_called()


Empty file added tests/bots/__init__.py
Empty file.
Loading

0 comments on commit d59fc4a

Please sign in to comment.