Exemple #1
0
 def log_run(self) -> None:
     """ Log timestamp and raiden version to help with debugging """
     version = get_system_spec()["raiden"]
     cursor = self.conn.cursor()
     cursor.execute("INSERT INTO runs(raiden_version) VALUES (?)",
                    [version])
     self.maybe_commit()
Exemple #2
0
def test_api_get_raiden_version(api_server_test_instance: APIServer):
    request = grequests.get(
        api_url_for(api_server_test_instance, "versionresource"))
    response = request.send().response
    assert_proper_response(response)

    raiden_version = get_system_spec()["raiden"]

    assert get_json_response(response) == {"version": raiden_version}
Exemple #3
0
    def run(self) -> None:
        configure_logging(
            self._options["log_config"],
            log_json=self._options["log_json"],
            log_file=self._options["log_file"],
            disable_debug_logfile=self._options["disable_debug_logfile"],
            debug_log_file_path=self._options["debug_logfile_path"],
        )

        log.info("Starting Raiden", **get_system_spec())

        if self._options["config_file"]:
            log.debug("Using config file", config_file=self._options["config_file"])
Exemple #4
0
def smoketest(
    ctx: Context, debug: bool, eth_client: EthClient, report_path: Optional[str]
) -> None:
    """ Test, that the raiden installation is sane. """
    from raiden.tests.utils.smoketest import (
        setup_raiden,
        run_smoketest,
        setup_matrix_for_smoketest,
        setup_testchain_for_smoketest,
    )
    from raiden.tests.utils.transport import make_requests_insecure, ParsedURL

    step_count = 8
    step = 0
    stdout = sys.stdout
    raiden_stdout = StringIO()

    assert ctx.parent, MYPY_ANNOTATION
    environment_type = ctx.parent.params["environment_type"]
    transport = ctx.parent.params["transport"]
    disable_debug_logfile = ctx.parent.params["disable_debug_logfile"]
    matrix_server = ctx.parent.params["matrix_server"]

    if transport != "matrix":
        raise RuntimeError(f"Invalid transport type '{transport}'")

    if report_path is None:
        report_file = mktemp(suffix=".log")
    else:
        report_file = report_path

    make_requests_insecure()
    urllib3.disable_warnings(InsecureRequestWarning)

    click.secho(f"Report file: {report_file}", fg="yellow")

    configure_logging(
        logger_level_config={"": "DEBUG"},
        log_file=report_file,
        disable_debug_logfile=disable_debug_logfile,
    )

    def append_report(subject: str, data: Optional[AnyStr] = None) -> None:
        with open(report_file, "a", encoding="UTF-8") as handler:
            handler.write(f'{f" {subject.upper()} ":=^80}{os.linesep}')
            if data is not None:
                write_data: str
                if isinstance(data, bytes):
                    write_data = data.decode()
                else:
                    write_data = data
                handler.writelines([write_data + os.linesep])

    append_report("Raiden version", json.dumps(get_system_spec()))
    append_report("Raiden log")

    def print_step(description: str, error: bool = False) -> None:
        nonlocal step
        step += 1
        click.echo(
            "{} {}".format(
                click.style(f"[{step}/{step_count}]", fg="blue"),
                click.style(description, fg="green" if not error else "red"),
            ),
            file=stdout,
        )

    contracts_version = RAIDEN_CONTRACT_VERSION

    try:
        free_port_generator = get_free_port()
        ethereum_nodes = None

        datadir = mkdtemp()
        testchain_manager: ContextManager[Dict[str, Any]] = setup_testchain_for_smoketest(
            eth_client=eth_client,
            print_step=print_step,
            free_port_generator=free_port_generator,
            base_datadir=datadir,
            base_logdir=datadir,
        )
        matrix_manager: ContextManager[
            List[Tuple[ParsedURL, HTTPExecutor]]
        ] = setup_matrix_for_smoketest(
            print_step=print_step,
            free_port_generator=free_port_generator,
            broadcast_rooms_aliases=[
                make_room_alias(NETWORKNAME_TO_ID["smoketest"], DISCOVERY_DEFAULT_ROOM),
                make_room_alias(NETWORKNAME_TO_ID["smoketest"], PATH_FINDING_BROADCASTING_ROOM),
            ],
        )

        # Do not redirect the stdout on a debug session, otherwise the REPL
        # will also be redirected
        if debug:
            stdout_manager = contextlib.nullcontext()
        else:
            stdout_manager = contextlib.redirect_stdout(raiden_stdout)

        with stdout_manager, testchain_manager as testchain, matrix_manager as server_urls:
            result = setup_raiden(
                transport=transport,
                matrix_server=matrix_server,
                print_step=print_step,
                contracts_version=contracts_version,
                eth_client=testchain["eth_client"],
                eth_rpc_endpoint=testchain["eth_rpc_endpoint"],
                web3=testchain["web3"],
                base_datadir=testchain["base_datadir"],
                keystore=testchain["keystore"],
            )

            args = result["args"]
            contract_addresses = result["contract_addresses"]
            ethereum_nodes = testchain["node_executors"]
            token = result["token"]

            port = next(free_port_generator)

            args["api_address"] = f"localhost:{port}"
            args["environment_type"] = environment_type

            # Matrix server
            # TODO: do we need more than one here?
            first_server = server_urls[0]
            args["matrix_server"] = first_server[0]
            args["one_to_n_contract_address"] = "0x" + "1" * 40
            args["routing_mode"] = RoutingMode.LOCAL
            args["flat_fee"] = ()
            args["proportional_fee"] = ()
            args["proportional_imbalance_fee"] = ()

            for option_ in run.params:
                if option_.name in args.keys():
                    args[option_.name] = option_.process_value(ctx, args[option_.name])
                else:
                    args[option_.name] = option_.default

            try:
                run_smoketest(
                    print_step=print_step,
                    args=args,
                    contract_addresses=contract_addresses,
                    token=token,
                )
            finally:
                if ethereum_nodes:
                    for node_executor in ethereum_nodes:
                        node = node_executor.process
                        node.send_signal(signal.SIGINT)

                        try:
                            node.wait(10)
                        except TimeoutExpired:
                            print_step("Ethereum node shutdown unclean, check log!", error=True)
                            node.kill()

                        if isinstance(node_executor.stdio, tuple):
                            logfile = node_executor.stdio[1]
                            logfile.flush()
                            logfile.seek(0)
                            append_report("Ethereum Node log output", logfile.read())

        append_report("Raiden Node stdout", raiden_stdout.getvalue())

    except:  # noqa pylint: disable=bare-except
        if debug:
            import pdb

            pdb.post_mortem()  # pylint: disable=no-member

        error = traceback.format_exc()
        append_report("Smoketest execution error", error)
        print_step("Smoketest execution error", error=True)
        success = False
    else:
        print_step(f"Smoketest successful")
        success = True

    if not success:
        sys.exit(1)
Exemple #5
0
def version(short: bool) -> None:
    """Print version information and exit. """
    if short:
        print(get_system_spec()["raiden"])
    else:
        print(json.dumps(get_system_spec(), indent=2))
Exemple #6
0
def run_services(options: Dict[str, Any]) -> None:
    if options["config_file"]:
        log.debug("Using config file", config_file=options["config_file"])

    app = run_app(**options)

    gevent_tasks: List[gevent.Greenlet] = list()

    if options["console"]:
        from raiden.ui.console import Console

        console = Console(app)
        console.start()

        gevent_tasks.append(console)

    gevent_tasks.append(spawn_named("check_version", check_version, get_system_spec()["raiden"]))
    gevent_tasks.append(spawn_named("check_gas_reserve", check_gas_reserve, app.raiden))
    gevent_tasks.append(
        spawn_named(
            "check_network_id",
            check_network_id,
            app.raiden.rpc_client.chain_id,
            app.raiden.rpc_client.web3,
        )
    )

    spawn_user_deposit_task = app.user_deposit and (
        options["pathfinding_service_address"] or options["enable_monitoring"]
    )
    if spawn_user_deposit_task:
        gevent_tasks.append(
            spawn_named("check_rdn_deposits", check_rdn_deposits, app.raiden, app.user_deposit)
        )

    stop_event: AsyncResult[Optional[signal.Signals]]  # pylint: disable=no-member
    stop_event = AsyncResult()

    def sig_set(sig: int, _frame: Any = None) -> None:
        stop_event.set(signal.Signals(sig))  # pylint: disable=no-member

    gevent.signal.signal(signal.SIGQUIT, sig_set)  # pylint: disable=no-member
    gevent.signal.signal(signal.SIGTERM, sig_set)  # pylint: disable=no-member
    gevent.signal.signal(signal.SIGINT, sig_set)  # pylint: disable=no-member

    # The SIGPIPE handler should not be installed. It is handled by the python
    # runtime, and an exception will be raised at the call site that triggered
    # the error.
    #
    # The default SIGPIPE handler set by the libc will terminate the process
    # [4]. However, the CPython interpreter changes the handler to IGN [3].
    # This allows for error reporting by the system calls that write to files.
    # Because of this, calling `send` to a closed socket will return an `EPIPE`
    # error [2], the error is then converted to an exception [5,6].
    #
    # 1 - https://github.com/python/cpython/blob/3.8/Modules/socketmodule.c#L4088
    # 2 - http://man7.org/linux/man-pages/man2/send.2.html
    # 3 - https://github.com/python/cpython/blob/3.8/Python/pylifecycle.c#L2306-L2307
    # 4 - https://www.gnu.org/software/libc/manual/html_node/Operation-Error-Signals.html
    # 5 - https://github.com/python/cpython/blob/3.8/Modules/socketmodule.c#L836-L838
    # 6 - https://github.com/python/cpython/blob/3.8/Modules/socketmodule.c#L627-L628
    # 7 - https://docs.python.org/3/library/signal.html#note-on-sigpipe
    #
    # gevent.signal.signal(signal.SIGPIPE, sig_set)  # pylint: disable=no-member

    # quit if any task exits, successfully or not
    app.raiden.greenlet.link(stop_event)
    for task in gevent_tasks:
        task.link(stop_event)

    try:
        signal_received = stop_event.get()
        if signal_received:
            print("\r", end="")  # Reset cursor to overwrite a possibly printed "^C"
            log.info("Signal received. Shutting down.", signal=signal_received)
    finally:
        for task in gevent_tasks:
            task.kill()

        app.raiden.stop()

        gevent.joinall(
            set(gevent_tasks + [app.raiden]), app.config.shutdown_timeout, raise_error=True
        )

        app.stop()
Exemple #7
0
def smoketest(ctx: Context, debug: bool, eth_client: EthClient,
              report_path: Optional[str]) -> None:  # pragma: no cover
    """ Test, that the raiden installation is sane. """
    from raiden.tests.utils.smoketest import run_smoketest, setup_smoketest, step_printer

    raiden_stdout = StringIO()

    assert ctx.parent, MYPY_ANNOTATION
    environment_type = ctx.parent.params["environment_type"]
    disable_debug_logfile = ctx.parent.params["disable_debug_logfile"]

    if report_path is None:
        report_file = mktemp(suffix=".log")
    else:
        report_file = report_path

    click.secho(f"Report file: {report_file}", fg="yellow")

    configure_logging(
        logger_level_config={"": "DEBUG"},
        log_file=report_file,
        disable_debug_logfile=disable_debug_logfile,
    )

    def append_report(subject: str, data: Optional[AnyStr] = None) -> None:
        with open(report_file, "a", encoding="UTF-8") as handler:
            handler.write(f'{f" {subject.upper()} ":=^80}{os.linesep}')
            if data is not None:
                write_data: str
                if isinstance(data, bytes):
                    write_data = data.decode()
                else:
                    write_data = data
                handler.writelines([write_data + os.linesep])

    append_report("Raiden version", json.dumps(get_system_spec()))
    append_report("Raiden log")

    free_port_generator = get_free_port()
    try:
        with step_printer(step_count=7, stdout=sys.stdout) as print_step:
            with setup_smoketest(
                    eth_client=eth_client,
                    print_step=print_step,
                    free_port_generator=free_port_generator,
                    debug=debug,
                    stdout=raiden_stdout,
                    append_report=append_report,
            ) as setup:
                args = setup.args
                port = next(free_port_generator)

                args["api_address"] = f"localhost:{port}"
                args["environment_type"] = environment_type

                # Matrix server
                args["one_to_n_contract_address"] = "0x" + "1" * 40
                args["routing_mode"] = RoutingMode.PRIVATE
                args["flat_fee"] = ()
                args["proportional_fee"] = ()
                args["proportional_imbalance_fee"] = ()

                for option_ in run.params:
                    if option_.name in args.keys():
                        args[option_.name] = option_.process_value(
                            ctx, args[option_.name])
                    else:
                        args[option_.name] = option_.default

                run_smoketest(print_step=print_step, setup=setup)

            append_report("Raiden Node stdout", raiden_stdout.getvalue())

    except:  # noqa pylint: disable=bare-except
        if debug:
            import pdb

            pdb.post_mortem()  # pylint: disable=no-member

        error = traceback.format_exc()
        append_report("Smoketest execution error", error)
        print_step("Smoketest execution error", error=True)
        success = False
    else:
        print_step("Smoketest successful")
        success = True

    if not success:
        sys.exit(1)
Exemple #8
0
def run(ctx: Context, **kwargs: Any) -> None:
    # pylint: disable=too-many-locals,too-many-branches,too-many-statements

    if kwargs["config_file"]:
        apply_config_file(run, kwargs, ctx)

    configure_logging(
        kwargs["log_config"],
        log_json=kwargs["log_json"],
        log_file=kwargs["log_file"],
        disable_debug_logfile=kwargs["disable_debug_logfile"],
        debug_log_file_path=kwargs["debug_logfile_path"],
    )

    flamegraph = kwargs.pop("flamegraph", None)
    switch_tracing = kwargs.pop("switch_tracing", None)
    profiler = None
    switch_monitor = None

    enable_gevent_monitoring_signal()

    if flamegraph:  # pragma: no cover
        windows_not_supported("flame graph")
        from raiden.utils.profiling.sampler import FlameGraphCollector, TraceSampler

        os.makedirs(flamegraph, exist_ok=True)

        now = datetime.datetime.now().isoformat()
        address = to_checksum_address(kwargs["address"])
        stack_path = os.path.join(flamegraph, f"{address}_{now}_stack.data")
        stack_stream = open(stack_path, "w")
        flame = FlameGraphCollector(stack_stream)
        profiler = TraceSampler(flame)

    if switch_tracing is True:  # pragma: no cover
        windows_not_supported("switch tracing")
        from raiden.utils.profiling.greenlets import SwitchMonitoring

        switch_monitor = SwitchMonitoring()

    if kwargs["environment_type"] == Environment.DEVELOPMENT:
        IDLE.enable()

    memory_logger = None
    log_memory_usage_interval = kwargs.pop("log_memory_usage_interval", 0)
    if log_memory_usage_interval > 0:  # pragma: no cover
        windows_not_supported("memory usage logging")
        from raiden.utils.profiling.memory import MemoryLogger

        memory_logger = MemoryLogger(log_memory_usage_interval)
        memory_logger.start()

    if ctx.invoked_subcommand is not None:
        # Pass parsed args on to subcommands.
        ctx.obj = kwargs
        return

    raiden_version = get_system_spec()["raiden"]
    click.secho(f"Welcome to Raiden, version {raiden_version}!", fg="green")

    click.secho(
        textwrap.dedent("""\
            +------------------------------------------------------------------------+
            | This is a Beta version of experimental open source software released   |
            | as a test version under an MIT license and may contain errors and/or   |
            | bugs. No guarantee or representation whatsoever is made regarding its  |
            | suitability (or its use) for any purpose or regarding its compliance   |
            | with any applicable laws and regulations. Use of the software is at    |
            | your own risk and discretion and by using the software you warrant and |
            | represent that you have read this disclaimer, understand its contents, |
            | assume all risk related thereto and hereby release, waive, discharge   |
            | and covenant not to hold liable Brainbot Labs Establishment or any of  |
            | its officers, employees or affiliates from and for any direct or       |
            | indirect damage resulting from the software or the use thereof.        |
            | Such to the extent as permissible by applicable laws and regulations.  |
            |                                                                        |
            | Privacy warning: Please be aware, that by using the Raiden Client,     |
            | among others your Ethereum address, channels, channel deposits,        |
            | settlements and the Ethereum address of your channel counterparty will |
            | be stored on the Ethereum chain, i.e. on servers of Ethereum node      |
            | operators and ergo are to a certain extent publicly available. The     |
            | same might also be stored on systems of parties running Raiden nodes   |
            | connected to the same token network. Data present in the Ethereum      |
            | chain is very unlikely to be able to be changed, removed or deleted    |
            | from the public arena.                                                 |
            |                                                                        |
            | Also be aware, that data on individual Raiden token transfers will be  |
            | made available via the Matrix protocol to the recipient,               |
            | intermediating nodes of a specific transfer as well as to the Matrix   |
            | server operators, see Raiden Transport Specification.                  |
            +------------------------------------------------------------------------+"""
                        ),
        fg="yellow",
    )
    if not kwargs["accept_disclaimer"]:
        click.confirm(
            "\nHave you read, understood and hereby accept the above "
            "disclaimer and privacy warning?",
            abort=True,
        )

    # Name used in the exception handlers, make sure the kwargs contains the
    # key with the correct name by always running it.
    name_or_id = ID_TO_CHAINNAME.get(kwargs["chain_id"], kwargs["chain_id"])

    # TODO:
    # - Ask for confirmation to quit if there are any locked transfers that did
    # not timeout.
    try:
        run_services(kwargs)
    except KeyboardInterrupt:
        # The user requested a shutdown. Assume that if the exception
        # propagated all the way to the top-level everything was shutdown
        # properly.
        #
        # Notes about edge cases:
        # - It could happen the exception was handled somewhere else in the
        # code, and did not reach the top-level, ideally that should result in
        # an exit with a non-zero code, but currently there is not way to
        # detect that.
        # - Just because the exception reached main, it doesn't mean that all
        # services were properly cleaned up. Ideally at this stage we should
        # run extra code to verify the state of the main services, and if any
        # of the is not properly shutdown exit with a non-zero code.
        pass
    except (ReplacementTransactionUnderpriced, EthereumNonceTooLow) as ex:
        click.secho(
            f"{ex}. Please make sure that this Raiden node is the "
            f"only user of the selected account",
            fg="red",
        )
        sys.exit(ReturnCode.ETH_ACCOUNT_ERROR)
    except (ConnectionError, ConnectTimeout, RequestsConnectionError,
            ReadTimeoutError):
        print(COMMUNICATION_ERROR.format(kwargs["eth_rpc_endpoint"]))
        sys.exit(ReturnCode.GENERIC_COMMUNICATION_ERROR)
    except EthNodeInterfaceError as e:
        click.secho(str(e), fg="red")
        sys.exit(ReturnCode.ETH_INTERFACE_ERROR)
    except RaidenUnrecoverableError as ex:
        write_stack_trace(ex)
        sys.exit(ReturnCode.FATAL)
    except APIServerPortInUseError as ex:
        click.secho(
            f"ERROR: API Address {ex} is in use. Use --api-address <host:port> "
            f"to specify a different port.",
            fg="red",
        )
        sys.exit(ReturnCode.PORT_ALREADY_IN_USE)
    except (KeystoreAuthenticationError, KeystoreFileNotFound) as e:
        click.secho(str(e), fg="red")
        sys.exit(ReturnCode.ETH_ACCOUNT_ERROR)
    except ConfigurationError as e:
        click.secho(str(e), fg="red")
        sys.exit(ReturnCode.RAIDEN_CONFIGURATION_ERROR)
    except ContractCodeMismatch as e:
        click.secho(
            f"{e}. This may happen if Raiden is configured to use an "
            f"unsupported version of the contracts.",
            fg="red",
        )
        sys.exit(ReturnCode.SMART_CONTRACTS_CONFIGURATION_ERROR)
    except AddressWithoutCode as e:
        click.secho(
            f"{e}. This may happen if an external ERC20 smart contract "
            f"selfdestructed, or if the configured address is misconfigured, make "
            f"sure the used address is not a normal account but a smart contract, "
            f"and that it is deployed to {name_or_id}.",
            fg="red",
        )
        sys.exit(ReturnCode.SMART_CONTRACTS_CONFIGURATION_ERROR)
    except filelock.Timeout:
        click.secho(
            f"FATAL: Another Raiden instance already running for account "
            f"{to_checksum_address(kwargs['address'])} on network id {name_or_id}",
            fg="red",
        )
        sys.exit(ReturnCode.RAIDEN_CONFIGURATION_ERROR)
    except Exception as ex:
        write_stack_trace(ex)
        sys.exit(ReturnCode.FATAL)
    finally:  # pragma: no cover
        # teardown order is important because of side-effects, both the
        # switch_monitor and profiler could use the tracing api, for the
        # teardown code to work correctly the teardown has to be done in the
        # reverse order of the initialization.
        if switch_monitor is not None:
            switch_monitor.stop()
        if memory_logger is not None:
            memory_logger.stop()
        if profiler is not None:
            profiler.stop()
Exemple #9
0
    def _start_services(self) -> None:
        if self._options["showconfig"]:
            print("Configuration Dump:")
            dump_cmd_options(self._options)
            dump_module("settings", settings)
            dump_module("constants", constants)

        app = run_app(**self._options)

        gevent_tasks: List[gevent.Greenlet] = list()
        runnable_tasks: List[Runnable] = list()

        runnable_tasks.append(app.raiden)

        domain_list = []
        if self._options["rpccorsdomain"]:
            if "," in self._options["rpccorsdomain"]:
                for domain in self._options["rpccorsdomain"].split(","):
                    domain_list.append(str(domain))
            else:
                domain_list.append(str(self._options["rpccorsdomain"]))

        self.raiden_api = RaidenAPI(app.raiden)

        if self._options["rpc"]:
            rest_api = RestAPI(self.raiden_api)
            (api_host, api_port) = split_endpoint(self._options["api_address"])

            if not api_port:
                api_port = Port(settings.DEFAULT_HTTP_SERVER_PORT)

            api_server = APIServer(
                rest_api,
                config={"host": api_host, "port": api_port},
                cors_domain_list=domain_list,
                web_ui=self._options["web_ui"],
                eth_rpc_endpoint=self._options["eth_rpc_endpoint"],
            )
            api_server.start()

            url = f"http://{api_host}:{api_port}/"
            print(
                f"The Raiden API RPC server is now running at {url}.\n\n See "
                f"the Raiden documentation for all available endpoints at\n "
                f"{DOC_URL}"
            )
            runnable_tasks.append(api_server)

        if self._options["console"]:
            from raiden.ui.console import Console

            console = Console(app)
            console.start()

            gevent_tasks.append(console)

        gevent_tasks.append(gevent.spawn(check_version, get_system_spec()["raiden"]))
        gevent_tasks.append(gevent.spawn(check_gas_reserve, app.raiden))
        gevent_tasks.append(
            gevent.spawn(
                check_network_id, app.raiden.rpc_client.chain_id, app.raiden.rpc_client.web3
            )
        )

        spawn_user_deposit_task = app.user_deposit and (
            self._options["pathfinding_service_address"] or self._options["enable_monitoring"]
        )
        if spawn_user_deposit_task:
            gevent_tasks.append(gevent.spawn(check_rdn_deposits, app.raiden, app.user_deposit))

        self._startup_hook()

        stop_event: AsyncResult[None] = AsyncResult()

        def sig_set(sig: Any = None, _frame: Any = None) -> None:
            stop_event.set(sig)

        gevent.signal(signal.SIGQUIT, sig_set)
        gevent.signal(signal.SIGTERM, sig_set)
        gevent.signal(signal.SIGINT, sig_set)

        # Make sure RaidenService is the last service in the list.
        runnable_tasks.reverse()

        # quit if any task exits, successfully or not
        for runnable in runnable_tasks:
            runnable.greenlet.link(stop_event)

        for task in gevent_tasks:
            task.link(stop_event)

        msg = (
            "The RaidenService must be last service to stop, since the other "
            "services depend on it to run. Without this it is not possible to have a "
            "clean shutdown, e.g. the RestAPI must be stopped before "
            "RaidenService, otherwise it is possible for a request to be "
            "processed after the RaidenService was stopped and it will cause a "
            "crash."
        )
        assert isinstance(runnable_tasks[-1], RaidenService), msg

        try:
            stop_event.get()
            print("Signal received. Shutting down ...")
        finally:
            self._shutdown_hook()

            for task in gevent_tasks:
                task.kill()

            for task in runnable_tasks:
                task.stop()

            gevent.joinall(
                set(gevent_tasks + runnable_tasks), app.config.shutdown_timeout, raise_error=True
            )

            app.stop()
Exemple #10
0
            expiration_block=self.expiration_block,
            chain_id=self.chain_id,
        )

        if self.signature is not None:
            data["signature"] = to_hex(self.signature)

        return data


USER_AGENT_STR = (
    (
        "Raiden/{raiden}/DB:{raiden_db_version}/{python_implementation}/"
        "{python_version}/{system}/{architecture}/{distribution}"
    )
    .format(**get_system_spec())
    .replace(" ", "-")
)

session = Session()
session.headers["User-Agent"] = USER_AGENT_STR
timeout_adapter = TimeoutHTTPAdapter(timeout=DEFAULT_HTTP_REQUEST_TIMEOUT)
session.mount("http://", timeout_adapter)
session.mount("https://", timeout_adapter)

MAX_PATHS_QUERY_ATTEMPTS = 2


def get_pfs_info(url: str) -> PFSInfo:
    try:
        response = session.get(f"{url}/api/v1/info")