From b34041b93e22ba9f17ea91f5294ce4a3df768fa6 Mon Sep 17 00:00:00 2001 From: joshuahamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:17:51 -0700 Subject: [PATCH 01/12] Add API key support for JSON-RPC requests This change allows `JsonRpcBase` to include an API key in headers when connecting to XRPL nodes that require authentication. The `__init__` method now accepts an `api_key` argument, which is stored as an attribute and used in `_request_impl`. - Updated the `_request_impl` method to add the `Authorization` header with the API key when provided. - Added an `api_key` parameter to the `__init__` method for easy instantiation with an API key. - This change was tested by connecting to a private XRPL server with required API authentication. --- xrpl/asyncio/clients/json_rpc_base.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index be6eb0605..427626597 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -3,6 +3,7 @@ from __future__ import annotations from json import JSONDecodeError +from typing import Optional from httpx import AsyncClient from typing_extensions import Self @@ -21,6 +22,17 @@ class JsonRpcBase(Client): :meta private: """ + def __init__(self: Self, url: str, api_key: Optional[str] = None) -> None: + """ + Initializes a new JsonRpcBase client. + + Arguments: + url: The URL of the XRPL node to connect to. + api_key: Optional API key for connecting to a private XRPL server. + """ + super().__init__(url) + self.api_key = api_key # Store the API key if provided + async def _request_impl( self: Self, request: Request, *, timeout: float = REQUEST_TIMEOUT ) -> Response: @@ -40,10 +52,18 @@ async def _request_impl( :meta private: """ + # Prepare headers, including the API key if it’s available + headers = {"Content-Type": "application/json"} + if self.api_key: + headers[ + "Authorization" + ] = f"Bearer {self.api_key}" # Adjust key name if necessary + async with AsyncClient(timeout=timeout) as http_client: response = await http_client.post( self.url, json=request_to_json_rpc(request), + headers=headers, # Include the headers with the optional API key ) try: return json_to_response(response.json()) From 09352855a3689828a34e3217f59a0aa9fb4a88a2 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:25:20 -0700 Subject: [PATCH 02/12] Response to comments on json_rpc_base.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Replaces api_key with headers: Optional[Dict[str, str]] • Merges global headers from the instance with per-request headers in _request_impl • Keeps the Content-Type: application/json header as the default base --- xrpl/asyncio/clients/json_rpc_base.py | 37 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index 427626597..da3132461 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -3,7 +3,7 @@ from __future__ import annotations from json import JSONDecodeError -from typing import Optional +from typing import Optional, Dict from httpx import AsyncClient from typing_extensions import Self @@ -22,19 +22,27 @@ class JsonRpcBase(Client): :meta private: """ - def __init__(self: Self, url: str, api_key: Optional[str] = None) -> None: + def __init__( + self: Self, + url: str, + headers: Optional[Dict[str, str]] = None, + ) -> None: """ Initializes a new JsonRpcBase client. Arguments: url: The URL of the XRPL node to connect to. - api_key: Optional API key for connecting to a private XRPL server. + headers: Optional default headers for all requests (e.g. API key or Dhali payment-claim). """ super().__init__(url) - self.api_key = api_key # Store the API key if provided + self.headers = headers or {} async def _request_impl( - self: Self, request: Request, *, timeout: float = REQUEST_TIMEOUT + self: Self, + request: Request, + *, + timeout: float = REQUEST_TIMEOUT, + headers: Optional[Dict[str, str]] = None, ) -> Response: """ Base ``_request_impl`` implementation for JSON RPC. @@ -42,7 +50,8 @@ async def _request_impl( Arguments: request: An object representing information about a rippled request. timeout: The duration within which we expect to hear a response from the - rippled validator. + rippled validator. + headers: Optional additional headers to include for this request. Returns: The response from the server, as a Response object. @@ -52,18 +61,18 @@ async def _request_impl( :meta private: """ - # Prepare headers, including the API key if it’s available - headers = {"Content-Type": "application/json"} - if self.api_key: - headers[ - "Authorization" - ] = f"Bearer {self.api_key}" # Adjust key name if necessary + # Merge global and per-request headers + merged_headers = { + "Content-Type": "application/json", + **self.headers, + **(headers or {}), + } async with AsyncClient(timeout=timeout) as http_client: response = await http_client.post( self.url, json=request_to_json_rpc(request), - headers=headers, # Include the headers with the optional API key + headers=merged_headers, ) try: return json_to_response(response.json()) @@ -73,4 +82,4 @@ async def _request_impl( "error": response.status_code, "error_message": response.text, } - ) + ) \ No newline at end of file From df4b392f4c91aa2d19b0f8e189b2795dedf13dfc Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:30:50 -0700 Subject: [PATCH 03/12] Revised per coderabbit nitpick Explicitly suppressed the traceback from the original `JSONDecodeError` by using `from None`, to keeps the error output clean and intentional. --- xrpl/asyncio/clients/json_rpc_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index da3132461..847d3cc89 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -82,4 +82,4 @@ async def _request_impl( "error": response.status_code, "error_message": response.text, } - ) \ No newline at end of file + ) from None \ No newline at end of file From 24a1187d378fe076252640d912696e819743971e Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:32:49 -0700 Subject: [PATCH 04/12] Update json_rpc_base.py Co-authored-by: Mayukha Vadari --- xrpl/asyncio/clients/json_rpc_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index 847d3cc89..622bf98d3 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -25,6 +25,7 @@ class JsonRpcBase(Client): def __init__( self: Self, url: str, + *, headers: Optional[Dict[str, str]] = None, ) -> None: """ From 3276a0a788d8002f40bd1c21837b0a01c3b3f989 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:33:06 -0700 Subject: [PATCH 05/12] Update json_rpc_base.py Co-authored-by: Mayukha Vadari --- xrpl/asyncio/clients/json_rpc_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index 622bf98d3..2b4342b04 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -51,7 +51,7 @@ async def _request_impl( Arguments: request: An object representing information about a rippled request. timeout: The duration within which we expect to hear a response from the - rippled validator. + rippled server. headers: Optional additional headers to include for this request. Returns: From 45b18556d8ef1932a5c571083818433c333afcca Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:15:28 -0700 Subject: [PATCH 06/12] Add headers support to base Client class for shared authentication functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change elevates support for custom HTTP headers to the base `Client` class, allowing all client implementations—HTTP and WebSocket—to access a shared headers attribute for authentication or metadata purposes. • Updated the `Client` constructor to accept an optional headers argument. • The headers attribute is now accessible from all subclasses, enabling future support in WebSocket clients. • Maintains full backward compatibility with existing clients. ⸻ ## High Level Overview of Change This PR introduces shared header support in the top-level `Client` class. The constructor now accepts an optional headers dictionary, which can be used to inject custom HTTP headers into XRPL client requests. This enables more flexible integration with infrastructure providers that require authentication tokens, API keys, or session-based headers. The change ensures that any future client—including WebSocket-based ones—can leverage the same shared configuration without duplicating logic in subclasses like `JsonRpcBase`. ⸻ ## Context of Change PR [763](https://github.com/XRPLF/xrpl-py/pull/763) would add header support only in `JsonRpcBase`, making it unavailable to other clients such as WebSocket-based ones. With XRPL infrastructure increasingly relying on authenticated access, this limitation restricted the usefulness of `xrpl-py` in production environments. By moving headers to the abstract base class, this change unifies the configuration mechanism and enables consistent authentication handling across all clients. ⸻ ## Type of Change [x] New feature (non-breaking change which adds functionality) ⸻ ## Did you update CHANGELOG.md? [x] Yes ⸻ ## Test Plan The change can be tested by: • Instantiating JsonRpcBase with a headers dictionary and verifying that headers are properly merged and included in `JSON-RPC` requests. • Creating a mock WebSocket client subclass that accesses `self.headers` and verifying shared availability. • Ensuring backward compatibility by initializing clients without the headers argument and observing unchanged behavior. ⸻ ## Future Tasks Future tasks related to this change could include: • Updating `WebSocketClient` to read from `self.headers` and send them during the handshake if supported. • Standardizing authentication strategies across all clients (e.g., Bearer tokens, session headers). • Adding test coverage for header usage across both sync and async clients. --- xrpl/asyncio/clients/client.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/xrpl/asyncio/clients/client.py b/xrpl/asyncio/clients/client.py index ed92f602b..f0ed50fff 100644 --- a/xrpl/asyncio/clients/client.py +++ b/xrpl/asyncio/clients/client.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Dict from typing_extensions import Final, Self @@ -24,14 +24,24 @@ class Client(ABC): :meta private: """ - def __init__(self: Self, url: str) -> None: + def __init__( + self: Self, + url: str, + headers: Optional[Dict[str, str]] = None, + ) -> None: """ Initializes a client. Arguments: - url: The url to which this client will connect + url: The URL to which this client will connect. + headers: Optional dictionary of default headers to include with each request. + These can be used to authenticate with private XRPL nodes or pass + custom metadata, such as: + - {"Authorization": "Bearer "} + - {"X-Dhali-Payment": ""} """ self.url = url + self.headers = headers or {} self.network_id: Optional[int] = None self.build_version: Optional[str] = None @@ -66,7 +76,6 @@ async def get_network_id_and_build_version(client: Client) -> None: Raises: XRPLRequestFailureException: if the rippled API call fails. """ - # the required values are already present, no need for further processing if client.network_id and client.build_version: return From a35dfd6a053a9baaf8fbf234f58db9807955d8f0 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:19:38 -0700 Subject: [PATCH 07/12] Update client.py --- xrpl/asyncio/clients/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/asyncio/clients/client.py b/xrpl/asyncio/clients/client.py index f0ed50fff..c53cebbbb 100644 --- a/xrpl/asyncio/clients/client.py +++ b/xrpl/asyncio/clients/client.py @@ -27,6 +27,7 @@ class Client(ABC): def __init__( self: Self, url: str, + *, headers: Optional[Dict[str, str]] = None, ) -> None: """ @@ -38,7 +39,6 @@ def __init__( These can be used to authenticate with private XRPL nodes or pass custom metadata, such as: - {"Authorization": "Bearer "} - - {"X-Dhali-Payment": ""} """ self.url = url self.headers = headers or {} From d69d73b90fdcc0fe5a2022ed8a86ec5f330a4d25 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:54:00 -0700 Subject: [PATCH 08/12] Update xrpl/asyncio/clients/json_rpc_base.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the need to assign `self.headers = headers or {}` again in `JsonRpcBase`, because it’s already handled in the parent class now. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- xrpl/asyncio/clients/json_rpc_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index 2b4342b04..ffc3fc536 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -35,9 +35,8 @@ def __init__( url: The URL of the XRPL node to connect to. headers: Optional default headers for all requests (e.g. API key or Dhali payment-claim). """ - super().__init__(url) + super().__init__(url, headers=headers) self.headers = headers or {} - async def _request_impl( self: Self, request: Request, From d800d7859720d0119f875096788eb13a4e06e74e Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:20:04 -0700 Subject: [PATCH 09/12] Update json_rpc_base.py Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> --- xrpl/asyncio/clients/json_rpc_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py index ffc3fc536..93ec7b920 100644 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ b/xrpl/asyncio/clients/json_rpc_base.py @@ -36,7 +36,6 @@ def __init__( headers: Optional default headers for all requests (e.g. API key or Dhali payment-claim). """ super().__init__(url, headers=headers) - self.headers = headers or {} async def _request_impl( self: Self, request: Request, From cfbc89e6a5d72bd9529337fb32d0181f7420dac7 Mon Sep 17 00:00:00 2001 From: Joshua Hamsa Date: Sun, 11 May 2025 21:32:35 -0700 Subject: [PATCH 10/12] Add unit tests for JsonRpcBase header support and auth handling --- .gitignore | 1 + tests/unit/asyn/clients/__init__.py | 0 tests/unit/asyn/clients/test_json_rpc_base.py | 77 +++++++++++++++++++ xrpl/asyncio/clients/exceptions.py | 6 ++ 4 files changed, 84 insertions(+) create mode 100644 tests/unit/asyn/clients/__init__.py create mode 100644 tests/unit/asyn/clients/test_json_rpc_base.py diff --git a/.gitignore b/.gitignore index 28809ddf1..f1812cd40 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,4 @@ dmypy.json # MacOS artifacts .DS_Store +.jrb/ .. diff --git a/tests/unit/asyn/clients/__init__.py b/tests/unit/asyn/clients/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/asyn/clients/test_json_rpc_base.py b/tests/unit/asyn/clients/test_json_rpc_base.py new file mode 100644 index 000000000..a22faf82c --- /dev/null +++ b/tests/unit/asyn/clients/test_json_rpc_base.py @@ -0,0 +1,77 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from httpx import Response + +from xrpl.asyncio.clients.exceptions import XRPLAuthenticationException +from xrpl.asyncio.clients.json_rpc_base import JsonRpcBase +from xrpl.models.requests import ServerInfo + + +@pytest.mark.asyncio +async def test_global_headers_are_sent(): + client = JsonRpcBase( + "https://xrpl.fake", headers={"Authorization": "Bearer testtoken"} + ) + + with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = Response( + status_code=200, + json={"result": {"status": "success"}, "id": 1}, + ) + + await client._request_impl(ServerInfo()) + + headers_sent = mock_post.call_args.kwargs["headers"] + assert headers_sent["Authorization"] == "Bearer testtoken" + assert headers_sent["Content-Type"] == "application/json" + + +@pytest.mark.asyncio +async def test_per_request_headers_override_global(): + client = JsonRpcBase( + "https://xrpl.fake", headers={"Authorization": "Bearer default"} + ) + + with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = Response( + status_code=200, + json={"result": {"status": "success"}, "id": 1}, + ) + + await client._request_impl( + ServerInfo(), headers={"Authorization": "Bearer override"} + ) + + headers_sent = mock_post.call_args.kwargs["headers"] + assert headers_sent["Authorization"] == "Bearer override" + + +@pytest.mark.asyncio +async def test_no_headers_does_not_crash(): + client = JsonRpcBase("https://xrpl.fake") + + with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = Response( + status_code=200, + json={"result": {"status": "success"}, "id": 1}, + ) + + await client._request_impl(ServerInfo()) + + headers_sent = mock_post.call_args.kwargs["headers"] + assert headers_sent["Content-Type"] == "application/json" + + +@pytest.mark.asyncio +async def test_raises_on_401_403(): + client = JsonRpcBase("https://xrpl.fake") + + for code in [401, 403]: + with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = Response(status_code=code, text="Unauthorized") + + with pytest.raises( + XRPLAuthenticationException, match="Authentication failed" + ): + await client._request_impl(ServerInfo()) diff --git a/xrpl/asyncio/clients/exceptions.py b/xrpl/asyncio/clients/exceptions.py index 82b664220..788fc8178 100644 --- a/xrpl/asyncio/clients/exceptions.py +++ b/xrpl/asyncio/clients/exceptions.py @@ -36,3 +36,9 @@ class XRPLWebsocketException(XRPLException): """ pass + + +class XRPLAuthenticationException(XRPLRequestFailureException): + """Raised when authentication with the XRPL node fails (401 or 403).""" + + pass From ef801bf5b38ecc740f097aad1cb93e5ce4afdcb7 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Sun, 11 May 2025 21:45:43 -0700 Subject: [PATCH 11/12] Delete xrpl/asyncio/clients/json_rpc_base.py; superseded by commit 6c8bab5. --- xrpl/asyncio/clients/json_rpc_base.py | 84 --------------------------- 1 file changed, 84 deletions(-) delete mode 100644 xrpl/asyncio/clients/json_rpc_base.py diff --git a/xrpl/asyncio/clients/json_rpc_base.py b/xrpl/asyncio/clients/json_rpc_base.py deleted file mode 100644 index 93ec7b920..000000000 --- a/xrpl/asyncio/clients/json_rpc_base.py +++ /dev/null @@ -1,84 +0,0 @@ -"""A common interface for JsonRpc requests.""" - -from __future__ import annotations - -from json import JSONDecodeError -from typing import Optional, Dict - -from httpx import AsyncClient -from typing_extensions import Self - -from xrpl.asyncio.clients.client import REQUEST_TIMEOUT, Client -from xrpl.asyncio.clients.exceptions import XRPLRequestFailureException -from xrpl.asyncio.clients.utils import json_to_response, request_to_json_rpc -from xrpl.models.requests.request import Request -from xrpl.models.response import Response - - -class JsonRpcBase(Client): - """ - A common interface for JsonRpc requests. - - :meta private: - """ - - def __init__( - self: Self, - url: str, - *, - headers: Optional[Dict[str, str]] = None, - ) -> None: - """ - Initializes a new JsonRpcBase client. - - Arguments: - url: The URL of the XRPL node to connect to. - headers: Optional default headers for all requests (e.g. API key or Dhali payment-claim). - """ - super().__init__(url, headers=headers) - async def _request_impl( - self: Self, - request: Request, - *, - timeout: float = REQUEST_TIMEOUT, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - """ - Base ``_request_impl`` implementation for JSON RPC. - - Arguments: - request: An object representing information about a rippled request. - timeout: The duration within which we expect to hear a response from the - rippled server. - headers: Optional additional headers to include for this request. - - Returns: - The response from the server, as a Response object. - - Raises: - XRPLRequestFailureException: if response can't be JSON decoded. - - :meta private: - """ - # Merge global and per-request headers - merged_headers = { - "Content-Type": "application/json", - **self.headers, - **(headers or {}), - } - - async with AsyncClient(timeout=timeout) as http_client: - response = await http_client.post( - self.url, - json=request_to_json_rpc(request), - headers=merged_headers, - ) - try: - return json_to_response(response.json()) - except JSONDecodeError: - raise XRPLRequestFailureException( - { - "error": response.status_code, - "error_message": response.text, - } - ) from None \ No newline at end of file From 3e7c197d506103a70e2ab200372bf807967991f5 Mon Sep 17 00:00:00 2001 From: Hamsa <108288013+joshuahamsa@users.noreply.github.com> Date: Sun, 11 May 2025 21:46:02 -0700 Subject: [PATCH 12/12] Delete xrpl/asyncio/clients/client.py; superseded by commit 6c8bab5. --- xrpl/asyncio/clients/client.py | 90 ---------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 xrpl/asyncio/clients/client.py diff --git a/xrpl/asyncio/clients/client.py b/xrpl/asyncio/clients/client.py deleted file mode 100644 index c53cebbbb..000000000 --- a/xrpl/asyncio/clients/client.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Interface for all network clients to follow.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Optional, Dict - -from typing_extensions import Final, Self - -from xrpl.asyncio.clients.exceptions import XRPLRequestFailureException -from xrpl.models.requests import ServerInfo -from xrpl.models.requests.request import Request -from xrpl.models.response import Response - -# The default request timeout duration. Set in Client._request_impl to allow more time -# for longer running commands. -REQUEST_TIMEOUT: Final[float] = 10.0 - - -class Client(ABC): - """ - Interface for all network clients to follow. - - :meta private: - """ - - def __init__( - self: Self, - url: str, - *, - headers: Optional[Dict[str, str]] = None, - ) -> None: - """ - Initializes a client. - - Arguments: - url: The URL to which this client will connect. - headers: Optional dictionary of default headers to include with each request. - These can be used to authenticate with private XRPL nodes or pass - custom metadata, such as: - - {"Authorization": "Bearer "} - """ - self.url = url - self.headers = headers or {} - self.network_id: Optional[int] = None - self.build_version: Optional[str] = None - - @abstractmethod - async def _request_impl( - self: Self, request: Request, *, timeout: float = REQUEST_TIMEOUT - ) -> Response: - """ - This is the actual driver for a given Client's request. It must be - async because all of the helper functions in this library are - async-first. Implement this in a given Client. - - Arguments: - request: An object representing information about a rippled request. - timeout: The maximum tolerable delay on waiting for a response. - - Returns: - The response from the server, as a Response object. - - :meta private: - """ - pass - - -async def get_network_id_and_build_version(client: Client) -> None: - """ - Get the network id and build version of the connected server. - - Args: - client: The network client to use to send the request. - - Raises: - XRPLRequestFailureException: if the rippled API call fails. - """ - if client.network_id and client.build_version: - return - - response = await client._request_impl(ServerInfo()) - if response.is_successful(): - if "network_id" in response.result["info"]: - client.network_id = response.result["info"]["network_id"] - if not client.build_version and "build_version" in response.result["info"]: - client.build_version = response.result["info"]["build_version"] - return - - raise XRPLRequestFailureException(response.result)