diff --git a/supra_sdk/clients/rest/general.py b/supra_sdk/clients/rest/general.py index bff1ba2..428c0a2 100644 --- a/supra_sdk/clients/rest/general.py +++ b/supra_sdk/clients/rest/general.py @@ -12,6 +12,7 @@ CHAIN_ID_ENDPOINT, EVENTS_BY_TYPE_ENDPOINT, TABLE_ITEMS_ENDPOINT, + VERSION_ENDPOINT, VIEW_FUNCTION_ENDPOINT, EventsPagination, format_endpoint, @@ -143,3 +144,24 @@ def iter_events_by_type( pagination or EventsPagination(), lambda p, cursor: replace(p, start=cursor), ) + + async def version(self) -> dict[str, Any]: + """Reads the node's version information from `GET /rpc/v3/version`. + + Returns: + dict[str, Any]: The node's response, with these fields: + - version (str): Version of the node's Rust packages, e.g. '0.5.0'. It is the same for every + release; use `release` to identify the release. + - release (str): Release tag the node binary was built from, e.g. 'supra_node_v12.0.0', or + 'supra_node_v12.0.0.rc7' for a release candidate. 'unreleased' for a binary built from a commit + that is not a release. The key is absent from the response of a node before + `supra_node_v12.0.0.rc7`: every v11 release and `supra_node_v12.0.0.rc1` to `rc6`. + - branch (str): Git branch used for the build. Empty when the build was made from a tag or the + branch was not recorded. + - commit_hash (str): Full git commit hash the binary was built from. Empty when the build did not + record it. + - build_time (str): Build timestamp. + - build_env (str): Rust toolchain used to build the binary. + + """ + return (await self.api_client.get(endpoint=VERSION_ENDPOINT)).json() diff --git a/supra_sdk/clients/rest/rest_types.py b/supra_sdk/clients/rest/rest_types.py index 13fc509..33cce4f 100644 --- a/supra_sdk/clients/rest/rest_types.py +++ b/supra_sdk/clients/rest/rest_types.py @@ -169,6 +169,9 @@ def format_endpoint(template: str, **values: Any) -> str: # View function endpoint VIEW_FUNCTION_ENDPOINT = f"{RPC_API_PREFIX}/view" +# Node version endpoint +VERSION_ENDPOINT = f"{RPC_API_PREFIX}/version" + # Table item endpoint TABLE_ITEMS_ENDPOINT = f"{RPC_API_PREFIX}/tables/{{table_handle}}/item" diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py new file mode 100644 index 0000000..bbd9f8d --- /dev/null +++ b/tests/unit/test_version.py @@ -0,0 +1,76 @@ +# Copyright © Supra +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from typing import Any + +import httpx + +from supra_sdk.clients import ApiClient +from supra_sdk.clients.rest.supra_client import SupraClient + +V11_RESPONSE = { + "version": "0.5.0", + "branch": "", + "commit_hash": "f52cb25b7dcdd4ad8b7d7530dbc185ecbeefda02", + "build_time": "2026-11-03 15:04:58 +10:00", + "build_env": "rustc 1.97.1 (8bab26f4f 2026-07-14)", +} + +V12_RESPONSE = { + "version": "0.5.0", + "release": "supra_node_v12.0.0.rc7", + "branch": "", + "commit_hash": "a89b26e61e6f8ff594f3d0435d9050f0b3488a1d", + "build_time": "2026-11-03 15:04:16 +10:00", + "build_env": "rustc 1.99.1 (58cf8f9c3 2026-10-13)", +} + + +async def _client_answering( + body: dict[str, Any], requests: list[httpx.Request] +) -> SupraClient: + """Builds a `SupraClient` whose HTTP transport records requests and answers every one with `body`.""" + + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, json=body) + + base_url = "http://node.example" + client = SupraClient(base_url) + config = client.api_client.api_client_config + await client.api_client.close() + client.api_client = ApiClient( + base_url, config, transport=httpx.MockTransport(handle) + ) + return client + + +class VersionTests(unittest.IsolatedAsyncioTestCase): + async def test_v12_response_carries_release(self): + requests: list[httpx.Request] = [] + client = await _client_answering(V12_RESPONSE, requests) + try: + result = await client.version() + finally: + await client.api_client.close() + self.assertEqual(result, V12_RESPONSE) + self.assertEqual(result["release"], "supra_node_v12.0.0.rc7") + self.assertEqual(len(requests), 1) + self.assertEqual(requests[0].method, "GET") + self.assertEqual(requests[0].url.path, "/rpc/v3/version") + + async def test_v11_response_has_no_release(self): + requests: list[httpx.Request] = [] + client = await _client_answering(V11_RESPONSE, requests) + try: + result = await client.version() + finally: + await client.api_client.close() + self.assertEqual(result, V11_RESPONSE) + self.assertNotIn("release", result) + self.assertEqual(requests[0].url.path, "/rpc/v3/version") + + +if __name__ == "__main__": + unittest.main()