diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 986fba7a..d32f50f4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -41,7 +41,7 @@ jobs: - name: Test with pytest working-directory: ct-app run: | - pytest test + pytest test -W ignore::DeprecationWarning image: name: Build and push container image diff --git a/ct-app/core/__main__.py b/ct-app/core/__main__.py index 9006cb2e..481be7fb 100644 --- a/ct-app/core/__main__.py +++ b/ct-app/core/__main__.py @@ -4,7 +4,7 @@ from prometheus_client import start_http_server from .components.parameters import Parameters -from .components.utils import EnvUtils, Utils +from .components.utils import EnvironmentUtils, Utils from .core import Core from .node import Node @@ -51,5 +51,5 @@ def main(): if __name__ == "__main__": - if EnvUtils.checkRequiredEnvVar("core"): + if EnvironmentUtils.checkRequiredEnvVar("core"): main() diff --git a/ct-app/core/components/environment_utils.py b/ct-app/core/components/environment_utils.py new file mode 100644 index 00000000..5035f814 --- /dev/null +++ b/ct-app/core/components/environment_utils.py @@ -0,0 +1,45 @@ +import subprocess +from os import environ +from typing import Any + +from .baseclass import Base + + +class EnvironmentUtils(Base): + def print_prefix(self) -> str: + return "EnvUtils" + + @classmethod + def envvar(cls, var_name: str, default: Any = None, type: type = str): + if var_name in environ: + return type(environ[var_name]) + else: + return default + + @classmethod + def envvarWithPrefix(cls, prefix: str, type=str) -> dict[str, Any]: + var_dict = { + key: type(v) for key, v in environ.items() if key.startswith(prefix) + } + + return dict(sorted(var_dict.items())) + + @classmethod + def checkRequiredEnvVar(cls, folder: str): + result = subprocess.run( + f"sh ./scripts/list_required_parameters.sh {folder}".split(), + capture_output=True, + text=True, + ).stdout + + all_set_flag = True + for var in result.splitlines(): + exists = var in environ + all_set_flag *= exists + + # print var with a leading check mark if it exists or red X (emoji) if it doesn't + cls().info(f"{'✅' if exists else '❌'} {var}") + + if not all_set_flag: + cls().error("Some required environment variables are not set.") + return all_set_flag diff --git a/ct-app/core/components/parameters.py b/ct-app/core/components/parameters.py index d1eeb930..4046a7bc 100644 --- a/ct-app/core/components/parameters.py +++ b/ct-app/core/components/parameters.py @@ -1,5 +1,5 @@ from .baseclass import Base -from .utils import EnvUtils +from .environment_utils import EnvironmentUtils class Parameters(Base): @@ -16,7 +16,7 @@ def __call__(self, *prefixes: str or list[str]): if subparams_name[-1] == "_": subparams_name = subparams_name[:-1] - for key, value in EnvUtils.envvarWithPrefix(prefix).items(): + for key, value in EnvironmentUtils.envvarWithPrefix(prefix).items(): k = key.replace(prefix, "").lower() try: diff --git a/ct-app/core/components/utils.py b/ct-app/core/components/utils.py index 96fbe4a4..1f7cf08b 100644 --- a/ct-app/core/components/utils.py +++ b/ct-app/core/components/utils.py @@ -2,10 +2,8 @@ import json import os import random -import subprocess import time from datetime import datetime, timedelta -from os import environ from typing import Any import aiohttp @@ -18,46 +16,8 @@ from core.model.topology_entry import TopologyEntry from .baseclass import Base - - -class EnvUtils(Base): - def print_prefix(self) -> str: - return "EnvUtils" - - @classmethod - def envvar(cls, var_name: str, default: Any = None, type: type = str): - if var_name in environ: - return type(environ[var_name]) - else: - return default - - @classmethod - def envvarWithPrefix(cls, prefix: str, type=str) -> dict[str, Any]: - var_dict = { - key: type(v) for key, v in environ.items() if key.startswith(prefix) - } - - return dict(sorted(var_dict.items())) - - @classmethod - def checkRequiredEnvVar(cls, folder: str): - result = subprocess.run( - f"sh ./scripts/list_required_parameters.sh {folder}".split(), - capture_output=True, - text=True, - ).stdout - - all_set_flag = True - for var in result.splitlines(): - exists = var in environ - all_set_flag *= exists - - # print var with a leading check mark if it exists or red X (emoji) if it doesn't - cls().info(f"{'✅' if exists else '❌'} {var}") - - if not all_set_flag: - cls().error("Some required environment variables are not set.") - return all_set_flag +from .channelstatus import ChannelStatus +from .environment_utils import EnvironmentUtils class Utils(Base): @@ -65,14 +25,27 @@ class Utils(Base): def nodesAddresses( cls, address_prefix: str, keyenv: str ) -> tuple[list[str], list[str]]: - addresses = EnvUtils.envvarWithPrefix(address_prefix).values() - keys = EnvUtils.envvarWithPrefix(keyenv).values() + """ + Returns a tuple containing the addresses and keys of the nodes. + :param address_prefix: The prefix of the environment variables containing addresses. + :param keyenv: The prefix of the environment variables containing keys. + :returns: A tuple containing the addresses and keys. + """ + addresses = EnvironmentUtils.envvarWithPrefix(address_prefix).values() + keys = EnvironmentUtils.envvarWithPrefix(keyenv).values() return list(addresses), list(keys) @classmethod - async def httpPOST(cls, url, data) -> tuple[int, dict]: - async def post(session: ClientSession, url: str, data: dict): + async def httpPOST(cls, url: str, data: dict) -> tuple[int, dict]: + """ + Performs an HTTP POST request. + :param url: The URL to send the request to. + :param data: The data to be sent. + :returns: A tuple containing the status code and the response. + """ + + async def _post(session: ClientSession, url: str, data: dict): async with session.post(url, json=data) as response: status = response.status response = await response.json() @@ -80,7 +53,7 @@ async def post(session: ClientSession, url: str, data: dict): async with aiohttp.ClientSession() as session: try: - status, response = await post(session, url, data) + status, response = await _post(session, url, data) except Exception: return None, None else: @@ -96,13 +69,12 @@ def mergeTopologyPeersSubgraph( """ Merge metrics and subgraph data with the unique peer IDs, addresses, balance links. - :param: topology_dict: A dict mapping peer IDs to node addresses. - :param: peers_list: A dict containing metrics with peer ID as the key. - :param: subgraph_dict: A dict containing subgraph data with safe address as key. + :param topology_dict: A dict mapping peer IDs to node addresses. + :param peers_list: A dict containing metrics with peer ID as the key. + :param subgraph_dict: A dict containing subgraph data with safe address as key. :returns: A dict with peer ID as the key and the merged information. """ merged_result: list[Peer] = [] - network_addresses = [p.address for p in peers_list] # Merge based on peer ID with the channel topology as the baseline @@ -133,7 +105,7 @@ def allowManyNodePerSafe(cls, peers: list[Peer]): """ Split the stake managed by a safe address equaly between the nodes that the safe manages. - :param: peer: list of peers + :param peer: list of peers :returns: nothing. """ safe_counts = {peer.safe_address: 0 for peer in peers} @@ -152,9 +124,9 @@ def excludeElements( ) -> list[Peer]: """ Removes elements from a dictionary based on a blacklist. - :param: source_data (dict): The dictionary to be updated. - :param: blacklist (list): A list containing the keys to be removed. - :returns: nothing. + :param source_data (dict): The dictionary to be updated. + :param blacklist (list): A list containing the keys to be removed. + :returns: A list containing the removed elements. """ peer_addresses = [peer.address for peer in source_data] @@ -176,8 +148,8 @@ def excludeElements( def rewardProbability(cls, peers: list[Peer]) -> list[int]: """ Evaluate the function for each stake value in the eligible_peers dictionary. - :param eligible_peers: A dict containing the data. - :returns: nothing. + :param peers: A dict containing the data. + :returns: A list containing the excluded elements due to low stake. """ indexes_to_remove = [ @@ -198,13 +170,12 @@ def rewardProbability(cls, peers: list[Peer]) -> list[int]: return excluded @classmethod - def jsonFromGCP(cls, bucket_name, blob_name, schema=None): + def jsonFromGCP(cls, bucket_name: str, blob_name: str): """ Reads a JSON file and validates its contents using a schema. - :param: bucket_name: The name of the bucket - :param: blob_name: The name of the blob - ;param: schema (opt): The validation schema - :returns: (dict): The contents of the JSON file. + :param bucket_name: The name of the bucket + :param blob_name: The name of the blob + :returns: The contents of the JSON file. """ storage_client = storage.Client() @@ -234,6 +205,14 @@ def stringArrayToGCP(cls, bucket_name: str, blob_name: str, data: list[str]): @classmethod def generateFilename(cls, prefix: str, foldername: str, extension: str = "csv"): + """ + Generates a filename with the following format: + _. + :param prefix: The prefix of the filename + :param foldername: The folder where the file will be stored + :param extension: The extension of the file + :returns: The filename + """ timestamp = time.strftime("%Y%m%d%H%M%S") if extension.startswith("."): @@ -247,6 +226,7 @@ def nextEpoch(cls, seconds: int) -> datetime: """ Calculates the delay until the next whole `minutes`min and `seconds`sec. :param seconds: next whole second to trigger the function + :returns: The next epoch """ if seconds == 0: raise ValueError("'seconds' must be greater than 0") @@ -261,6 +241,7 @@ def nextDelayInSeconds(cls, seconds: int) -> int: """ Calculates the delay until the next whole `minutes`min and `seconds`sec. :param seconds: next whole second to trigger the function + :returns: The delay in seconds. """ if seconds == 0: return 1 @@ -276,6 +257,8 @@ def nextDelayInSeconds(cls, seconds: int) -> int: async def aggregatePeerBalanceInChannels(cls, channels: list) -> dict[str, dict]: """ Returns a dict containing all unique source_peerId-source_address links. + :param channels: The list of channels. + :returns: A dict containing all peerIds-balanceInChannels links. """ results: dict[str, dict] = {} @@ -284,10 +267,11 @@ async def aggregatePeerBalanceInChannels(cls, channels: list) -> dict[str, dict] hasattr(c, "source_peer_id") and hasattr(c, "source_address") and hasattr(c, "status") + and hasattr(c, "balance") ): continue - if c.status != "Open": + if ChannelStatus(c.status) != ChannelStatus.Open: continue if c.source_peer_id not in results: @@ -301,15 +285,18 @@ async def aggregatePeerBalanceInChannels(cls, channels: list) -> dict[str, dict] return results @classmethod - def splitDict(cls, peers: dict[str, int], bins: int) -> list[dict]: + def splitDict(cls, src: dict[str, Any], bins: int) -> list[dict[str, Any]]: """ - Splits randomly a dict into multiple sub-dictionary. + Splits randomly a dict into multiple sub-dictionary of almost equal sizes. + :param src: The dict to be split. + :param bins: The number of sub-dictionaries. + :returns: A list containing the sub-dictionaries. """ # Split the dictionary into multiple sub-dictionaries - split = [{} for i in range(bins)] + split = [{} for _ in range(bins)] # Assign a random number to each element in the dictionary - for peer_id, data in peers.items(): - split[random.randint(0, bins - 1)][peer_id] = data + for idx, (key, value) in enumerate(random.sample(src.items(), len(src))): + split[idx % bins][key] = value return split diff --git a/ct-app/postman/postman_tasks.py b/ct-app/postman/postman_tasks.py index 9cf860f3..26cedb7b 100644 --- a/ct-app/postman/postman_tasks.py +++ b/ct-app/postman/postman_tasks.py @@ -7,7 +7,7 @@ from celery import Celery from core.components.hoprd_api import HoprdAPI from core.components.parameters import Parameters -from core.components.utils import EnvUtils, Utils +from core.components.utils import EnvironmentUtils, Utils from database import DatabaseConnection, Reward from .task_status import TaskStatus @@ -18,7 +18,7 @@ params = Parameters()("PARAM_", "RABBITMQ_") -if not EnvUtils.checkRequiredEnvVar("postman"): +if not EnvironmentUtils.checkRequiredEnvVar("postman"): exit(1) app = Celery( diff --git a/ct-app/test/components/test_channelstatus.py b/ct-app/test/components/test_channelstatus.py index a57b3ff7..a249535b 100644 --- a/ct-app/test/components/test_channelstatus.py +++ b/ct-app/test/components/test_channelstatus.py @@ -2,7 +2,14 @@ def test_channelstatus(): - assert ChannelStatus.isPending("PendingToClose") - assert not ChannelStatus.isPending("Open") - assert not ChannelStatus.isOpen("PendingToClose") assert ChannelStatus.isOpen("Open") + assert not ChannelStatus.isOpen("PendingToClose") + assert not ChannelStatus.isOpen("Closed") + + assert not ChannelStatus.isPending("Open") + assert ChannelStatus.isPending("PendingToClose") + assert not ChannelStatus.isPending("Closed") + + assert not ChannelStatus.isClosed("Open") + assert not ChannelStatus.isClosed("PendingToClose") + assert ChannelStatus.isClosed("Closed") diff --git a/ct-app/test/components/test_environment_utils.py b/ct-app/test/components/test_environment_utils.py new file mode 100644 index 00000000..2a375d76 --- /dev/null +++ b/ct-app/test/components/test_environment_utils.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path + +from core.components.utils import EnvironmentUtils + + +def test_envvar(): + os.environ["STRING_ENVVAR"] = "string-envvar" + os.environ["INT_ENVVAR"] = "1" + os.environ["FLOAT_ENVVAR"] = "1.0" + + assert EnvironmentUtils.envvar("FAKE_STRING_ENVVAR", "default") == "default" + assert EnvironmentUtils.envvar("STRING_ENVVAR", type=str) == "string-envvar" + assert EnvironmentUtils.envvar("INT_ENVVAR", type=int) == 1 + assert EnvironmentUtils.envvar("FLOAT_ENVVAR", type=float) == 1.0 + + del os.environ["STRING_ENVVAR"] + del os.environ["INT_ENVVAR"] + del os.environ["FLOAT_ENVVAR"] + + +def test_envvarWithPrefix(): + os.environ["TEST_ENVVAR_2"] = "2" + os.environ["TEST_ENVVAR_1"] = "1" + os.environ["TEST_ENVVAR_3"] = "3" + os.environ["TEST_ENVVOR_4"] = "3" + + assert EnvironmentUtils.envvarWithPrefix("TEST_ENVVAR_", type=int) == { + "TEST_ENVVAR_1": 1, + "TEST_ENVVAR_2": 2, + "TEST_ENVVAR_3": 3, + } + + del os.environ["TEST_ENVVAR_1"] + del os.environ["TEST_ENVVAR_2"] + del os.environ["TEST_ENVVAR_3"] + del os.environ["TEST_ENVVOR_4"] + + +def test_checkRequiredEnvVar(): + test_folder = Path(__file__).parent.joinpath("test_code") + file = test_folder.joinpath("test_main.py") + test_folder.mkdir(exist_ok=False) + file.touch(exist_ok=False) + + file.write_text( + """ + var1 = params.group1.var1 + var2 = params.group1.var2 + var3 = params.group2.var1 + var4 = params.var1 + """ + ) + + assert not EnvironmentUtils.checkRequiredEnvVar(test_folder) + + os.environ["GROUP1_VAR1"] = "val11" + os.environ["GROUP1_VAR2"] = "val12" + os.environ["GROUP2_VAR1"] = "val21" + os.environ["VAR1"] = "val1" + + assert EnvironmentUtils.checkRequiredEnvVar(test_folder) + + # cleanup + file.unlink() + test_folder.rmdir() diff --git a/ct-app/test/components/test_utils.py b/ct-app/test/components/test_utils.py index 232c1c36..918babaf 100644 --- a/ct-app/test/components/test_utils.py +++ b/ct-app/test/components/test_utils.py @@ -1,43 +1,80 @@ import datetime import os +import random import pytest -from core.components.utils import EnvUtils, Utils +from core.components.utils import Utils from core.model.address import Address from core.model.peer import Peer - - -def test_envvar(): - os.environ["STRING_ENVVAR"] = "string-envvar" - os.environ["INT_ENVVAR"] = "1" - os.environ["FLOAT_ENVVAR"] = "1.0" - - assert EnvUtils.envvar("FAKE_STRING_ENVVAR", "default") == "default" - assert EnvUtils.envvar("STRING_ENVVAR", type=str) == "string-envvar" - assert EnvUtils.envvar("INT_ENVVAR", type=int) == 1 - assert EnvUtils.envvar("FLOAT_ENVVAR", type=float) == 1.0 - - del os.environ["STRING_ENVVAR"] - del os.environ["INT_ENVVAR"] - del os.environ["FLOAT_ENVVAR"] - - -def test_envvarWithPrefix(): - os.environ["TEST_ENVVAR_2"] = "2" - os.environ["TEST_ENVVAR_1"] = "1" - os.environ["TEST_ENVVAR_3"] = "3" - os.environ["TEST_ENVVOR_4"] = "3" - - assert EnvUtils.envvarWithPrefix("TEST_ENVVAR_", type=int) == { - "TEST_ENVVAR_1": 1, - "TEST_ENVVAR_2": 2, - "TEST_ENVVAR_3": 3, - } - - del os.environ["TEST_ENVVAR_1"] - del os.environ["TEST_ENVVAR_2"] - del os.environ["TEST_ENVVAR_3"] - del os.environ["TEST_ENVVOR_4"] +from core.model.subgraph_entry import SubgraphEntry +from core.model.topology_entry import TopologyEntry +from hoprd_sdk.models import ChannelTopology + + +@pytest.fixture +def channel_topology(): + return [ + ChannelTopology( + "channel_1", + "src_1", + "dst_1", + "src_addr_1", + "dst_addr_1", + f"{1*1e18:.0f}", + "Open", + "", + "", + "", + ), + ChannelTopology( + "channel_2", + "src_1", + "dst_2", + "src_addr_1", + "dst_addr_2", + f"{2*1e18:.0f}", + "Open", + "", + "", + "", + ), + ChannelTopology( + "channel_3", + "src_1", + "dst_3", + "src_addr_1", + "dst_addr_3", + f"{3*1e18:.0f}", + "Closed", + "", + "", + "", + ), + ChannelTopology( + "channel_4", + "src_2", + "dst_1", + "src_addr_2", + "dst_addr_1", + f"{4*1e18:.0f}", + "Open", + "", + "", + "", + ), + ChannelTopology( + "channel_5", + "src_2", + "dst_2", + "src_addr_2", + "dst_addr_2", + f"{1*1e18:.0f}", + "Open", + "", + "", + "", + ), + ] def test_nodeAddresses(): @@ -61,7 +98,25 @@ def test_httpPOST(): def test_mergeTopologyPeersSubgraph(): - pytest.skip("Not implemented") + topology_list = [ + TopologyEntry(None, None, 1), + TopologyEntry("peer_id_2", "address_2", 2), + TopologyEntry("peer_id_3", "address_3", 3), + TopologyEntry("peer_id_4", "address_4", 4), + ] + peers_list = [ + Peer("peer_id_1", "address_1", "1.0.0"), + Peer("peer_id_2", "address_2", "1.1.0"), + Peer("peer_id_3", "address_3", "1.0.2"), + ] + subgraph_list = [ + SubgraphEntry("address_1", "10", "safe_address_1", "1"), + SubgraphEntry("address_2", "10", "safe_address_2", "2"), + SubgraphEntry("address_3", None, "safe_address_3", "3"), + ] + + merged = Utils.mergeTopologyPeersSubgraph(topology_list, peers_list, subgraph_list) + assert len(merged) == 1 def test_allowManyNodePerSafe(): @@ -105,7 +160,7 @@ def test_excludeElements(): def test_rewardProbability(): - pass + pytest.skip("Not implemented") def test_jsonFromGCP(): @@ -145,13 +200,22 @@ def test_nextDelayInSeconds(): assert delay == 1 -def test_aggregatePeerBalanceInChannels(): - pytest.skip("Not implemented") +@pytest.mark.asyncio +async def test_aggregatePeerBalanceInChannels(channel_topology): + results = await Utils.aggregatePeerBalanceInChannels(channel_topology) + assert len(results) == 2 + assert results["src_1"]["channels_balance"] == 3 + assert results["src_2"]["channels_balance"] == 5 -def test_taskSendMessage(): - pytest.skip("Not implemented") +def test_splitDict(): + bins = random.randint(2, 10) + num_elements = random.randint(50, 100) + source_dict = {f"key_{i}": f"value_{i}" for i in range(num_elements)} -def test_taskStoreFeedback(): - pytest.skip("Not implemented") + result = Utils.splitDict(source_dict, bins) + key_counts = [len(item.keys()) for item in result] + + assert len(result) == bins + assert max(key_counts) - min(key_counts) <= 1 diff --git a/ct-app/tests_endurance/module/endurance_test.py b/ct-app/tests_endurance/module/endurance_test.py index 2212f146..3ea22046 100644 --- a/ct-app/tests_endurance/module/endurance_test.py +++ b/ct-app/tests_endurance/module/endurance_test.py @@ -4,7 +4,7 @@ import time from datetime import timedelta -from core.components.utils import EnvUtils +from core.components.utils import EnvironmentUtils from .metric import Metric @@ -27,8 +27,12 @@ def __init__(self, duration: int, rate: float): self.metric_list: list[Metric] = [] self._progress_bar_length = 45 - log.setLevel(getattr(logging, EnvUtils.envvar("LOG_LEVEL", default="INFO"))) - log.disabled = not EnvUtils.envvar("LOG_ENABLED", type=bool, default=True) + log.setLevel( + getattr(logging, EnvironmentUtils.envvar("LOG_LEVEL", default="INFO")) + ) + log.disabled = not EnvironmentUtils.envvar( + "LOG_ENABLED", type=bool, default=True + ) async def progress_bar(self): """ diff --git a/ct-app/tests_endurance/test_fund_channels.py b/ct-app/tests_endurance/test_fund_channels.py index f7847cb7..3c1c90c1 100644 --- a/ct-app/tests_endurance/test_fund_channels.py +++ b/ct-app/tests_endurance/test_fund_channels.py @@ -2,7 +2,7 @@ import random from core.components.hoprd_api import HoprdAPI -from core.components.utils import EnvUtils +from core.components.utils import EnvironmentUtils from . import EnduranceTest, Metric @@ -10,7 +10,9 @@ class FundChannels(EnduranceTest): async def on_start(self): self.results = [] - self.api = HoprdAPI(EnvUtils.envvar("API_URL"), EnvUtils.envvar("API_KEY")) + self.api = HoprdAPI( + EnvironmentUtils.envvar("API_URL"), EnvironmentUtils.envvar("API_KEY") + ) address = await self.api.get_address("hopr") self.info(f"Connected to node '...{address[-10:]}'") @@ -31,7 +33,7 @@ async def on_start(self): async def task(self) -> bool: success = await self.api.fund_channel( - self.channel.id, EnvUtils.envvar("FUND_AMOUNT") + self.channel.id, EnvironmentUtils.envvar("FUND_AMOUNT") ) self.results.append(success) @@ -47,7 +49,7 @@ async def balance_changed(id: str, balance: str): return channel.balance - timeout = EnvUtils.envvar("BALANCE_CHANGE_TIMEOUT", float) + timeout = EnvironmentUtils.envvar("BALANCE_CHANGE_TIMEOUT", float) self.info(f"Waiting up to {timeout}s for the balance to change") try: self.final_balance = await asyncio.wait_for( diff --git a/ct-app/tests_endurance/test_send_messages.py b/ct-app/tests_endurance/test_send_messages.py index 637cf7bd..17afde17 100644 --- a/ct-app/tests_endurance/test_send_messages.py +++ b/ct-app/tests_endurance/test_send_messages.py @@ -2,7 +2,7 @@ import random from core.components.hoprd_api import HoprdAPI -from core.components.utils import EnvUtils +from core.components.utils import EnvironmentUtils from . import EnduranceTest, Metric @@ -12,7 +12,9 @@ async def on_start(self): self.results = [] self.tag = random.randint(0, 2**16 - 1) - self.api = HoprdAPI(EnvUtils.envvar("API_URL"), EnvUtils.envvar("API_KEY")) + self.api = HoprdAPI( + EnvironmentUtils.envvar("API_URL"), EnvironmentUtils.envvar("API_KEY") + ) self.recipient = await self.api.get_address("hopr") channels = await self.api.all_channels(False) @@ -25,7 +27,7 @@ async def on_start(self): if len(open_channels) == 0: raise Exception("No open channels found") - self.relayer = EnvUtils.envvar("RELAYER_PEER_ID", None) + self.relayer = EnvironmentUtils.envvar("RELAYER_PEER_ID", None) if self.relayer is None: channel = random.choice(open_channels) self.relayer = channel.destination_peer_id @@ -54,7 +56,7 @@ async def task(self) -> bool: self.results.append(success) async def on_end(self): - sleep_time = EnvUtils.envvar("DELAY_BEFORE_INBOX_CHECK", type=float) + sleep_time = EnvironmentUtils.envvar("DELAY_BEFORE_INBOX_CHECK", type=float) if sum(self.results) > 0: self.info(f"Waiting {sleep_time}s for messages to be relayed")