Skip to content
This repository was archived by the owner on May 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions reticulum_openapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,55 @@ def _build_link_destination(self, dest_identity: RNS.Identity) -> RNS.Destinatio
"link",
)

async def _resolve_destination_identity(
self, dest_hex: str, dest_hash: bytes, timeout: float
) -> RNS.Identity:
"""Return the announced identity for ``dest_hash`` waiting if needed.

Args:
dest_hex (str): Hexadecimal representation of the destination hash.
dest_hash (bytes): Binary destination hash.
timeout (float): Maximum seconds to wait for an announce.

Returns:
RNS.Identity: The recalled destination identity.

Raises:
TimeoutError: If the identity is not announced before ``timeout``.
"""

identity = RNS.Identity.recall(dest_hash)
if identity is not None:
return identity

deadline = self._loop.time() + timeout
request_interval = min(1.0, max(0.5, timeout))
next_request = 0.0

while True:
remaining = deadline - self._loop.time()
if remaining <= 0:
raise TimeoutError(
"Destination identity "
f"{dest_hex} was not announced within {timeout} seconds"
)

if self._loop.time() >= next_request:
try:
RNS.Transport.request_path(dest_hash)
except Exception: # pragma: no cover - defensive logging path
logger.debug(
"Failed to request path for destination %s",
dest_hex,
exc_info=True,
)
next_request = self._loop.time() + request_interval

await asyncio.sleep(min(0.2, remaining))
identity = RNS.Identity.recall(dest_hash)
if identity is not None:
return identity

async def _ensure_link(
self, dest_hex: str, dest_hash: bytes, timeout: float
) -> RNS.Link:
Expand All @@ -176,8 +225,8 @@ async def _ensure_link(
link = self._links.get(dest_hash)
event = self._link_events.get(dest_hash)
if link is None or event is None:
dest_identity = RNS.Identity.recall(dest_hash) or RNS.Identity.recall(
dest_hash, create=True
dest_identity = await self._resolve_destination_identity(
dest_hex, dest_hash, timeout
)
destination = self._build_link_destination(dest_identity)
event = asyncio.Event()
Expand Down
58 changes: 52 additions & 6 deletions tests/test_client_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,7 @@ async def test_send_command_bytes_payload(monkeypatch):
cli.auth_token = None
cli.timeout = 0.2

monkeypatch.setattr(
client_module.RNS.Identity, "recall", lambda h, create=False: object()
)
cli._resolve_destination_identity = AsyncMock(return_value=object())

class FakeDestination:
OUT = object()
Expand Down Expand Up @@ -202,9 +200,7 @@ async def test_send_command_dict_payload(monkeypatch):
cli.auth_token = "secret"
cli.timeout = 0.2

monkeypatch.setattr(
client_module.RNS.Identity, "recall", lambda h, create=False: object()
)
cli._resolve_destination_identity = AsyncMock(return_value={"id": 1})

class FakeDestination:
OUT = object()
Expand Down Expand Up @@ -404,6 +400,56 @@ async def test_wait_for_server_announce_timeout(monkeypatch):
await client.wait_for_server_announce(timeout=0.05)


@pytest.mark.asyncio
async def test_resolve_destination_identity_requests_path(monkeypatch):
loop = asyncio.get_running_loop()
client = client_module.LXMFClient.__new__(client_module.LXMFClient)
client._loop = loop
identity = object()
recall_results = [None, identity]

def fake_recall(_hash):
if recall_results:
return recall_results.pop(0)
return identity

path_requests = []

monkeypatch.setattr(client_module.RNS.Identity, "recall", fake_recall)
monkeypatch.setattr(
client_module.RNS.Transport,
"request_path",
lambda dest: path_requests.append(dest),
)

result = await client._resolve_destination_identity(
"abcd", bytes.fromhex("abcd"), 0.3
)

assert result is identity
assert path_requests


@pytest.mark.asyncio
async def test_resolve_destination_identity_timeout(monkeypatch):
loop = asyncio.get_running_loop()
client = client_module.LXMFClient.__new__(client_module.LXMFClient)
client._loop = loop

monkeypatch.setattr(client_module.RNS.Identity, "recall", lambda _hash: None)
path_requests = []
monkeypatch.setattr(
client_module.RNS.Transport,
"request_path",
lambda dest: path_requests.append(dest),
)

with pytest.raises(TimeoutError):
await client._resolve_destination_identity("abcd", bytes.fromhex("abcd"), 0.15)

assert path_requests


@pytest.mark.asyncio
async def test_discover_server_identity_returns_hex(monkeypatch):
register_calls = _patch_dependencies(monkeypatch)
Expand Down
Loading