From c75f170ed02f8da187fc82c79342d6cfde8fdba3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 31 Jan 2026 06:59:27 +0800 Subject: [PATCH] Refactor: Move Discovery and Nostr logic to routstr/nostr package --- routstr/core/main.py | 4 +- routstr/core/settings.py | 2 +- routstr/nostr/__init__.py | 4 ++ routstr/{ => nostr}/discovery.py | 6 +- routstr/{nip91.py => nostr/listing.py} | 50 ++++++++-------- tests/integration/test_provider_management.py | 58 +++++++++++-------- 6 files changed, 69 insertions(+), 55 deletions(-) create mode 100644 routstr/nostr/__init__.py rename routstr/{ => nostr}/discovery.py (98%) rename routstr/{nip91.py => nostr/listing.py} (91%) diff --git a/routstr/core/main.py b/routstr/core/main.py index c18eb76a..f45924b6 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -11,8 +11,8 @@ from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException from ..balance import balance_router, deprecated_wallet_router -from ..discovery import providers_cache_refresher, providers_router -from ..nip91 import announce_provider +from ..nostr import announce_provider, providers_cache_refresher +from ..nostr.discovery import providers_router from ..payment.models import models_router, update_sats_pricing from ..payment.price import update_prices_periodically from ..proxy import initialize_upstreams, proxy_router, refresh_model_maps_periodically diff --git a/routstr/core/settings.py b/routstr/core/settings.py index 9fce51e8..cd38c0c9 100644 --- a/routstr/core/settings.py +++ b/routstr/core/settings.py @@ -143,7 +143,7 @@ def resolve_bootstrap() -> Settings: pass if not base.onion_url: try: - from ..nip91 import discover_onion_url_from_tor # type: ignore + from ..nostr.listing import discover_onion_url_from_tor # type: ignore discovered = discover_onion_url_from_tor() if discovered: diff --git a/routstr/nostr/__init__.py b/routstr/nostr/__init__.py new file mode 100644 index 00000000..b19039a7 --- /dev/null +++ b/routstr/nostr/__init__.py @@ -0,0 +1,4 @@ +from .discovery import providers_cache_refresher +from .listing import announce_provider + +__all__ = ["providers_cache_refresher", "announce_provider"] diff --git a/routstr/discovery.py b/routstr/nostr/discovery.py similarity index 98% rename from routstr/discovery.py rename to routstr/nostr/discovery.py index 94375b63..268b4c7c 100644 --- a/routstr/discovery.py +++ b/routstr/nostr/discovery.py @@ -8,8 +8,8 @@ import httpx import websockets from fastapi import APIRouter, HTTPException -from .core.logging import get_logger -from .core.settings import settings +from ..core.logging import get_logger +from ..core.settings import settings logger = get_logger(__name__) @@ -72,8 +72,6 @@ async def query_nostr_relay_for_providers( elif data[0] == "NOTICE": try: msg = str(data[1]) - if len(msg) > 200: - msg = msg[:200] + "..." logger.debug(f"Relay notice: {msg}") except Exception: logger.debug("Relay notice received") diff --git a/routstr/nip91.py b/routstr/nostr/listing.py similarity index 91% rename from routstr/nip91.py rename to routstr/nostr/listing.py index d14c2c6e..a512add7 100644 --- a/routstr/nip91.py +++ b/routstr/nostr/listing.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -NIP-91: Routstr Provider Discoverability Implementation +Listing: Routstr Provider Discoverability Implementation Automatically announces this Routstr proxy instance to Nostr relays. """ @@ -18,15 +18,15 @@ from nostr.key import PrivateKey from nostr.message_type import ClientMessageType from nostr.relay_manager import RelayManager -from .core import get_logger -from .core.settings import settings +from ..core import get_logger +from ..core.settings import settings logger = get_logger(__name__) def get_app_version() -> str | None: try: - from .core.main import __version__ as imported_version + from ..core.main import __version__ as imported_version return imported_version except Exception: @@ -71,7 +71,7 @@ def nsec_to_keypair(nsec: str) -> tuple[str, str] | None: return None -def create_nip91_event( +def create_listing_event( private_key_hex: str, provider_id: str, endpoint_urls: list[str], @@ -80,7 +80,7 @@ def create_nip91_event( metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: """ - Create a NIP-91 compliant provider announcement event (kind:38421). + Create a listing provider announcement event (kind:38421). Args: private_key_hex: 32-byte hex private key for signing @@ -164,14 +164,14 @@ def events_semantically_equal(a: dict[str, Any], b: dict[str, Any]) -> bool: return True -async def query_nip91_events( +async def query_listing_events( relay_url: str, pubkey: str, provider_id: str | None = None, timeout: int = 30, ) -> tuple[list[dict[str, Any]], bool]: """ - Query a Nostr relay for NIP-91 provider announcements (kind:38421) via nostr library. + Query a Nostr relay for listing provider announcements (kind:38421) via nostr library. Returns a tuple of (events, ok) where ok indicates whether the relay interaction succeeded without transport-level errors. @@ -188,7 +188,7 @@ async def query_nip91_events( flt = Filter(kinds=[38421], authors=[pubkey], limit=10) filters = Filters([flt]) - sub_id = f"nip91_{int(time.time())}" + sub_id = f"routstr_listing_{int(time.time())}" rm.add_subscription(sub_id, filters) req: list[Any] = [ClientMessageType.REQUEST, sub_id] req.extend(filters.to_json_array()) @@ -294,7 +294,7 @@ async def _determine_provider_id(public_key_hex: str, relay_urls: list[str]) -> async def query_single_relay(relay_url: str) -> list[dict[str, Any]]: try: - events, _ok = await query_nip91_events(relay_url, public_key_hex, None) + events, _ok = await query_listing_events(relay_url, public_key_hex, None) return events except Exception: return [] @@ -330,7 +330,7 @@ async def publish_to_relay( timeout: int = 30, ) -> bool: """ - Publish a NIP-91 event to a nostr relay via nostr library. + Publish a listing event to a nostr relay via nostr library. """ def _sync_publish() -> bool: @@ -341,7 +341,7 @@ async def publish_to_relay( time.sleep(1.0) # Publish the event as-is via publish_message to preserve signature rm.publish_message(json.dumps(["EVENT", event])) - logger.debug(f"Sent NIP-91 event {event.get('id', '')} to {relay_url}") + logger.debug(f"Sent listing event {event.get('id', '')} to {relay_url}") time.sleep(1.0) return True except Exception as e: @@ -364,13 +364,13 @@ async def announce_provider() -> None: # Check for NSEC in environment (use NSEC only) nsec = settings.nsec if not nsec: - logger.info("Nostr private key not found (NSEC), skipping NIP-91 announcement") + logger.info("Nostr private key not found (NSEC), skipping listing announcement") return # Convert NSEC to keypair keypair = nsec_to_keypair(nsec) if not keypair: - logger.error("Failed to parse NSEC, skipping NIP-91 announcement") + logger.error("Failed to parse NSEC, skipping listing announcement") return private_key_hex, public_key_hex = keypair @@ -409,7 +409,7 @@ async def announce_provider() -> None: if not endpoint_urls: logger.warning( - "No valid endpoints configured (HTTP_URL/ONION_URL). Skipping NIP-91 publish." + "No valid endpoints configured (HTTP_URL/ONION_URL). Skipping listing publish." ) return @@ -434,7 +434,7 @@ async def announce_provider() -> None: # Create the candidate event that we would publish version_str = get_app_version() - candidate_event = create_nip91_event( + candidate_event = create_listing_event( private_key_hex=private_key_hex, provider_id=provider_id, endpoint_urls=endpoint_urls, @@ -474,7 +474,7 @@ async def announce_provider() -> None: if _should_skip(relay_url): logger.debug(f"Skipping {relay_url} due to backoff") continue - events, ok = await query_nip91_events(relay_url, public_key_hex, provider_id) + events, ok = await query_listing_events(relay_url, public_key_hex, provider_id) if ok: _register_success(relay_url) existing_events.extend(events) @@ -489,7 +489,7 @@ async def announce_provider() -> None: if not all_match: logger.debug( - "No matching NIP-91 announcement found or differences detected; publishing update" + "No matching listing announcement found or differences detected; publishing update" ) success_count = 0 for relay_url in relay_urls: @@ -502,11 +502,11 @@ async def announce_provider() -> None: else: _register_failure(relay_url) logger.info( - f"Published NIP-91 announcement to {success_count}/{len(relay_urls)} relays" + f"Published listing announcement to {success_count}/{len(relay_urls)} relays" ) else: logger.debug( - "Matching NIP-91 announcement already present; skipping publish on startup" + "Matching listing announcement already present; skipping publish on startup" ) # Re-announce periodically (every 24 hours) @@ -518,7 +518,7 @@ async def announce_provider() -> None: # Build fresh candidate event for comparison version_str = get_app_version() - candidate_event = create_nip91_event( + candidate_event = create_listing_event( private_key_hex=private_key_hex, provider_id=provider_id, endpoint_urls=endpoint_urls, @@ -533,7 +533,7 @@ async def announce_provider() -> None: if _should_skip(relay_url): logger.debug(f"Skipping {relay_url} due to backoff") continue - events, ok = await query_nip91_events( + events, ok = await query_listing_events( relay_url, public_key_hex, provider_id ) if ok: @@ -549,7 +549,7 @@ async def announce_provider() -> None: if all_match: logger.debug( - "Matching NIP-91 announcement already present; skipping periodic re-announce" + "Matching listing announcement already present; skipping periodic re-announce" ) continue @@ -567,8 +567,8 @@ async def announce_provider() -> None: _register_failure(relay_url) except asyncio.CancelledError: - logger.info("NIP-91 announcement task cancelled") + logger.info("Listing announcement task cancelled") break except Exception as e: - logger.debug(f"Error in NIP-91 announcement loop: {type(e).__name__}") + logger.debug(f"Error in listing announcement loop: {type(e).__name__}") # Continue running despite errors diff --git a/tests/integration/test_provider_management.py b/tests/integration/test_provider_management.py index 584fd8c1..b47ad848 100644 --- a/tests/integration/test_provider_management.py +++ b/tests/integration/test_provider_management.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest from httpx import AsyncClient -from routstr.discovery import _PROVIDERS_CACHE +from routstr.nostr.discovery import _PROVIDERS_CACHE from .utils import ResponseValidator @@ -71,9 +71,10 @@ async def test_providers_endpoint_default_response( } with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: # Configure mock to return appropriate responses mock_fetch.side_effect = lambda url: mock_fetch_responses.get( url, {"status_code": 500, "json": {"error": "Unknown provider"}} @@ -135,9 +136,10 @@ async def test_providers_endpoint_with_include_json( } with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = { "status_code": 200, "json": mock_provider_response, @@ -209,9 +211,10 @@ async def test_providers_data_structure_validation( } with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = mock_health_response response = await integration_client.get("/v1/providers/?include_json=true") @@ -256,7 +259,8 @@ async def test_providers_endpoint_no_providers_found( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): response = await integration_client.get("/v1/providers/") @@ -317,10 +321,11 @@ async def test_providers_endpoint_offline_providers( } with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): with patch( - "routstr.discovery.fetch_provider_health", + "routstr.nostr.discovery.fetch_provider_health", side_effect=mock_fetch_provider_health, ): response = await integration_client.get("/v1/providers/?include_json=true") @@ -386,9 +391,10 @@ async def test_providers_endpoint_duplicate_urls( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = { "status_code": 200, "endpoint": "root", @@ -425,7 +431,8 @@ async def test_providers_endpoint_nostr_relay_failures( raise Exception("Connection to relay failed") with patch( - "routstr.discovery.query_nostr_relay_for_providers", side_effect=failing_query + "routstr.nostr.discovery.query_nostr_relay_for_providers", + side_effect=failing_query, ): response = await integration_client.get("/v1/providers/") @@ -463,9 +470,10 @@ async def test_providers_endpoint_malformed_urls( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}} response = await integration_client.get("/v1/providers/") @@ -495,9 +503,10 @@ async def test_providers_endpoint_response_format( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}} # Test default format @@ -545,9 +554,10 @@ async def test_providers_endpoint_concurrent_requests( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}} # Create concurrent requests @@ -587,9 +597,10 @@ async def test_providers_endpoint_parameter_validation( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}} # Test various parameter values @@ -639,9 +650,10 @@ async def test_no_database_changes_during_provider_operations( ] with patch( - "routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events + "routstr.nostr.discovery.query_nostr_relay_for_providers", + return_value=mock_events, ): - with patch("routstr.discovery.fetch_provider_health") as mock_fetch: + with patch("routstr.nostr.discovery.fetch_provider_health") as mock_fetch: mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}} # Make multiple requests with different parameters