From 436e3ce30ef8c63d5b046ac80a54812e50f69d84 Mon Sep 17 00:00:00 2001 From: 0xFantomMenace <93462858+0xFantomMenace@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:35:54 +0000 Subject: [PATCH] URL-encode the HIP-4 outcome slug --- .gitignore | 1 + CHANGELOG.md | 2 ++ oxarchive/resources/outcomes.py | 5 +++-- tests/test_slug_encoding.py | 21 +++++++++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/test_slug_encoding.py diff --git a/.gitignore b/.gitignore index 3d9c9d2..9e55087 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ tests/* !tests/_mock_api.py !tests/test_timestamps.py !tests/test_spec_fixes.py +!tests/test_slug_encoding.py !tests/test_websocket_full_depth.py !tests/test_webhooks.py !tests/test_webhook_signature.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f30f8..e46d055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,6 +222,8 @@ deployments in the SDK, mainnet (`client.lighter`) and Robinhood Chain `OrdersResource`, and Hyperliquid core keeps `LiquidationsResource`. ### Fixed +- `hip4.outcomes.get_by_slug()` URL-encodes the slug. Slugs with spaces, + colons, `#` or `/` reached the wrong route before. - `trades.list()` and `alist()` send `cursor` back exactly as the API returned it. A trades cursor is an opaque string such as `"1790640000578_218303497631402"` (Lighter adds a third part), and it was diff --git a/oxarchive/resources/outcomes.py b/oxarchive/resources/outcomes.py index da4ecf5..eebf680 100644 --- a/oxarchive/resources/outcomes.py +++ b/oxarchive/resources/outcomes.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, List, Optional +from urllib.parse import quote from ..http import HttpClient from ..types import CursorResponse, Hip4OutcomeAggregate, Hip4Question, ResponseMeta @@ -96,12 +97,12 @@ def get_by_slug(self, slug: str) -> Hip4OutcomeAggregate: :class:`Hip4OutcomeAggregate` with ``aggregated_oi`` populated, like :py:meth:`get`. """ - data = self._http.get(f"{self._base_path}/outcomes/by-slug/{slug}") + data = self._http.get(f"{self._base_path}/outcomes/by-slug/{quote(slug, safe='')}") return Hip4OutcomeAggregate.model_validate(data["data"]) async def aget_by_slug(self, slug: str) -> Hip4OutcomeAggregate: """Async version of get_by_slug().""" - data = await self._http.aget(f"{self._base_path}/outcomes/by-slug/{slug}") + data = await self._http.aget(f"{self._base_path}/outcomes/by-slug/{quote(slug, safe='')}") return Hip4OutcomeAggregate.model_validate(data["data"]) diff --git a/tests/test_slug_encoding.py b/tests/test_slug_encoding.py new file mode 100644 index 0000000..0fe5b06 --- /dev/null +++ b/tests/test_slug_encoding.py @@ -0,0 +1,21 @@ +"""HIP-4 outcome by slug: the slug is URL-encoded as one path segment.""" + +import asyncio + +from tests._mock_api import envelope, mock_client + +SLUG = "june-fed-rate-change-no change: #2/3" +ENCODED = "/v1/hyperliquid/hip4/outcomes/by-slug/june-fed-rate-change-no%20change%3A%20%232%2F3" + + +def test_get_by_slug_encodes_the_slug() -> None: + client, api = mock_client(lambda p, q: envelope({"outcome_id": 1, "sides": []})) + try: + client.hyperliquid.hip4.outcomes.get_by_slug(SLUG) + except Exception: + pass # response parsing is not under test; the request path is + try: + asyncio.run(client.hyperliquid.hip4.outcomes.aget_by_slug(SLUG)) + except Exception: + pass + assert api.raw_paths[:2] == [ENCODED, ENCODED]