diff --git a/core/scripts/generate-python-exchanges.js b/core/scripts/generate-python-exchanges.js index ee0739d9..55b21f87 100644 --- a/core/scripts/generate-python-exchanges.js +++ b/core/scripts/generate-python-exchanges.js @@ -137,45 +137,80 @@ function generateClass(exchange) { const extraAttrs = []; const credOverrideLines = []; + // Track which params have been added to avoid duplicates + const addedParams = new Set(); + if (creds.apiKey) { constructorParams.push('api_key: Optional[str] = None'); superArgs.push('api_key=api_key'); + addedParams.add('api_key'); } if (creds.apiToken) { constructorParams.push('api_token: Optional[str] = None'); superArgs.push('api_token=api_token'); + addedParams.add('api_token'); } if (creds.apiSecret) { constructorParams.push('api_secret: Optional[str] = None'); extraAttrs.push('self.api_secret = api_secret'); credOverrideLines.push(' if self.api_secret:', ' creds["apiSecret"] = self.api_secret'); + addedParams.add('api_secret'); } if (creds.passphrase) { constructorParams.push('passphrase: Optional[str] = None'); extraAttrs.push('self.passphrase = passphrase'); credOverrideLines.push(' if self.passphrase:', ' creds["passphrase"] = self.passphrase'); + addedParams.add('passphrase'); } if (creds.privateKey) { const pyParam = aliases['private_key'] || 'private_key'; const defaultVal = defaults['private_key'] || 'None'; constructorParams.push(`${pyParam}: Optional[str] = ${defaultVal}`); superArgs.push(`private_key=${pyParam}`); + addedParams.add(pyParam); } if (creds.funderAddress) { constructorParams.push('proxy_address: Optional[str] = None'); superArgs.push('proxy_address=proxy_address'); + addedParams.add('proxy_address'); } if (creds.signatureType) { const defaultVal = defaults['signature_type'] || 'None'; constructorParams.push(`signature_type: Optional[str] = ${defaultVal}`); superArgs.push('signature_type=signature_type'); + addedParams.add('signature_type'); } + constructorParams.push('base_url: Optional[str] = None'); constructorParams.push('auto_start_server: Optional[bool] = None'); constructorParams.push('pmxt_api_key: Optional[str] = None'); superArgs.push('base_url=base_url'); superArgs.push('auto_start_server=auto_start_server'); superArgs.push('pmxt_api_key=pmxt_api_key'); + addedParams.add('base_url'); + addedParams.add('auto_start_server'); + addedParams.add('pmxt_api_key'); + + // 👇 ONLY ADD wallet_address IF NOT ALREADY ADDED VIA ALIAS (e.g., Myriad) + if (!addedParams.has('wallet_address')) { + constructorParams.push('wallet_address: Optional[str] = None'); + superArgs.push('wallet_address=wallet_address'); + addedParams.add('wallet_address'); + } + + // 👇 ONLY ADD signer IF NOT ALREADY ADDED + if (!addedParams.has('signer')) { + constructorParams.push('signer: Optional[object] = None'); + superArgs.push('signer=signer'); + addedParams.add('signer'); + } + + // 👇 ONLY ADD websocket IF NOT ALREADY ADDED + if (!addedParams.has('websocket')) { + constructorParams.push('websocket: Optional[dict] = None'); + superArgs.push('websocket=websocket'); + addedParams.add('websocket'); + } const docLines = []; if (creds.apiKey) docLines.push(' api_key: API key for authentication (optional)'); @@ -192,6 +227,17 @@ function generateClass(exchange) { docLines.push(' base_url: Base URL of the PMXT sidecar server'); docLines.push(' auto_start_server: Automatically start server if not running (default: True)'); docLines.push(' pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode)'); + + // 👇 ONLY ADD DOCS IF THE PARAM EXISTS + if (addedParams.has('wallet_address')) { + docLines.push(' wallet_address: Wallet address for hosted operations (optional)'); + } + if (addedParams.has('signer')) { + docLines.push(' signer: Custom signer for hosted operations (optional)'); + } + if (addedParams.has('websocket')) { + docLines.push(' websocket: WebSocket configuration dict (optional)'); + } const indent4 = s => ` ${s}`; const indent8 = s => ` ${s}`; diff --git a/sdks/python/pmxt/_exchanges.py b/sdks/python/pmxt/_exchanges.py index b6402ad9..f845eae7 100644 --- a/sdks/python/pmxt/_exchanges.py +++ b/sdks/python/pmxt/_exchanges.py @@ -21,10 +21,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, - # NOTE: Generated wrapper; update the generator template in - # core/scripts/generate-python-exchanges.js in a follow-up. wallet_address: Optional[str] = None, signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Polymarket client. @@ -39,8 +38,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) - wallet_address: Ethereum address for hosted reads/writes (optional) - signer: Optional callable for signing typed_data (optional) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="polymarket", @@ -53,6 +53,7 @@ def __init__( pmxt_api_key=pmxt_api_key, wallet_address=wallet_address, signer=signer, + websocket=websocket, ) self.api_secret = api_secret @@ -86,6 +87,7 @@ def __init__( pmxt_api_key: Optional[str] = None, wallet_address: Optional[str] = None, signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Limitless client. @@ -98,8 +100,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) - wallet_address: Ethereum address for hosted reads/writes (optional) - signer: Optional callable for signing typed_data (optional) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="limitless", @@ -110,6 +113,7 @@ def __init__( pmxt_api_key=pmxt_api_key, wallet_address=wallet_address, signer=signer, + websocket=websocket, ) self.api_secret = api_secret @@ -134,6 +138,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Kalshi client. @@ -144,6 +151,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="kalshi", @@ -152,6 +162,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -165,6 +178,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize KalshiDemo client. @@ -175,6 +191,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="kalshi-demo", @@ -183,6 +202,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -198,6 +220,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Probable client. @@ -210,6 +235,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="probable", @@ -218,6 +246,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) self.api_secret = api_secret @@ -241,6 +272,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Baozi client. @@ -250,6 +284,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="baozi", @@ -257,6 +294,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -270,6 +310,8 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Myriad client. @@ -280,6 +322,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="myriad", @@ -288,6 +333,8 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + signer=signer, + websocket=websocket, ) @@ -302,10 +349,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, - # NOTE: Generated wrapper; update the generator template in - # core/scripts/generate-python-exchanges.js in a follow-up. wallet_address: Optional[str] = None, signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Opinion client. @@ -317,8 +363,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) - wallet_address: Ethereum address for hosted reads/writes (optional) - signer: Optional callable for signing typed_data (optional) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="opinion", @@ -330,6 +377,7 @@ def __init__( pmxt_api_key=pmxt_api_key, wallet_address=wallet_address, signer=signer, + websocket=websocket, ) @@ -342,6 +390,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Metaculus client. @@ -351,6 +402,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="metaculus", @@ -358,6 +412,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -371,6 +428,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Smarkets client. @@ -381,6 +441,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="smarkets", @@ -389,6 +452,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -402,6 +468,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize PolymarketUS client. @@ -412,6 +481,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="polymarket_us", @@ -420,6 +492,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -433,6 +508,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Hyperliquid client. @@ -443,6 +521,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="hyperliquid", @@ -451,6 +532,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -464,6 +548,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize GeminiTitan client. @@ -474,6 +561,9 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="gemini-titan", @@ -481,6 +571,9 @@ def __init__( base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) self.api_secret = api_secret @@ -500,6 +593,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize SuiBets client. @@ -508,12 +604,18 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="suibets", base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -522,23 +624,35 @@ class Rain(Exchange): def __init__( self, + private_key: Optional[str] = None, base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Rain client. Args: + private_key: Private key for authentication (optional) base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="rain", + private_key=private_key, base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -550,6 +664,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Mock client. @@ -558,12 +675,18 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="mock", base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) @@ -575,6 +698,9 @@ def __init__( base_url: Optional[str] = None, auto_start_server: Optional[bool] = None, pmxt_api_key: Optional[str] = None, + wallet_address: Optional[str] = None, + signer: Optional[object] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize Router client. @@ -583,12 +709,18 @@ def __init__( base_url: Base URL of the PMXT sidecar server auto_start_server: Automatically start server if not running (default: True) pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + wallet_address: Wallet address for hosted operations (optional) + signer: Custom signer for hosted operations (optional) + websocket: WebSocket configuration dict (optional) """ super().__init__( exchange_name="router", base_url=base_url, auto_start_server=auto_start_server, pmxt_api_key=pmxt_api_key, + wallet_address=wallet_address, + signer=signer, + websocket=websocket, ) # Backwards-compatible aliases for exchange classes generated before underscore handling. diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index cca8c805..bef28170 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -334,6 +334,7 @@ def __init__( pmxt_api_key: Optional[str] = None, wallet_address: Optional[str] = None, signer: Optional[Any] = None, + websocket: Optional[dict] = None, ) -> None: """ Initialize an exchange client. @@ -376,6 +377,7 @@ def __init__( self.markets: Dict[str, "UnifiedMarket"] = {} self.markets_by_slug: Dict[str, "UnifiedMarket"] = {} self._loaded_markets: bool = False + self.websocket = websocket # Sticky flag: flipped to True the first time the sidecar rejects a # GET read with 404/405 (i.e. an older pmxt-core that only supports # POST). Once set, read methods skip the GET probe for the lifetime @@ -2327,13 +2329,13 @@ def _get_or_create_ws(self): host = self._resolve_sidecar_host() if self.is_hosted: - client = SidecarWsClient(host, api_key=self.pmxt_api_key) + client = SidecarWsClient(host, api_key=self.pmxt_api_key,config=self.websocket,) else: server_info = self._server_manager.get_server_info() access_token = ( server_info.get("accessToken") if server_info else None ) - client = SidecarWsClient(host, access_token=access_token) + client = SidecarWsClient(host, access_token=access_token,config=self.websocket,) try: # Trigger connection to validate the endpoint exists with client._lock: diff --git a/sdks/python/pmxt/ws_client.py b/sdks/python/pmxt/ws_client.py index 30710f55..53c4708d 100644 --- a/sdks/python/pmxt/ws_client.py +++ b/sdks/python/pmxt/ws_client.py @@ -19,7 +19,7 @@ from .errors import PmxtError MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION = 100_000 -CONNECT_ATTEMPTS = 3 +CONNECT_ATTEMPTS = 3 # Left for fallback defaults _NO_DATA = object() @@ -66,7 +66,7 @@ class SidecarWsClient: may invoke subscribe/receive from any thread. """ - def __init__(self, host: str, access_token: Optional[str] = None, api_key: Optional[str] = None) -> None: + def __init__(self, host: str, access_token: Optional[str] = None, api_key: Optional[str] = None,config: Optional[dict] = None,) -> None: self._host = host self._access_token = access_token self._api_key = api_key @@ -84,6 +84,7 @@ def __init__(self, host: str, access_token: Optional[str] = None, api_key: Optio # Track active subscriptions by (method, symbol_key) -> request_id # to avoid duplicate subscribe messages for the same ticker self._active_subs: Dict[str, str] = {} + self._config = config or {} # ------------------------------------------------------------------ # Connection lifecycle @@ -102,24 +103,48 @@ def _ensure_connected(self) -> None: "Install it with: pip install websocket-client" ) - scheme = "ws" - # Strip http(s):// prefix from host to build ws URL - host_part = self._host - if host_part.startswith("https://"): - host_part = host_part[len("https://"):] - scheme = "wss" - elif host_part.startswith("http://"): - host_part = host_part[len("http://"):] - - url = f"{scheme}://{host_part}/ws" - if self._api_key: - url = f"{url}?apiKey={self._api_key}" - elif self._access_token: - url = f"{url}?token={self._access_token}" + # ✅ CHECK FOR CUSTOM WS URL FROM CONFIG + custom_ws_url = self._config.get("wsUrl") if self._config else None + + if custom_ws_url: + # Use custom WebSocket URL + url = custom_ws_url + # Append auth parameters if not already in URL + if self._api_key: + if "?" in url: + url = f"{url}&apiKey={self._api_key}" + else: + url = f"{url}?apiKey={self._api_key}" + elif self._access_token: + if "?" in url: + url = f"{url}&token={self._access_token}" + else: + url = f"{url}?token={self._access_token}" + else: + # Build default URL from host + scheme = "ws" + host_part = self._host + if host_part.startswith("https://"): + host_part = host_part[len("https://"):] + scheme = "wss" + elif host_part.startswith("http://"): + host_part = host_part[len("http://"):] + + url = f"{scheme}://{host_part}/ws" + if self._api_key: + url = f"{url}?apiKey={self._api_key}" + elif self._access_token: + url = f"{url}?token={self._access_token}" + + # ✅ Get reconnect settings from config (defaulting to 3 attempts to match legacy behavior) + reconnect_interval = self._config.get("reconnectInterval", 5000) if self._config else 5000 + max_reconnect_attempts = self._config.get("maxReconnectAttempts", CONNECT_ATTEMPTS) if self._config else CONNECT_ATTEMPTS last_error: Optional[Exception] = None ws = None - for attempt in range(CONNECT_ATTEMPTS): + + # ✅ FIXED: Use max_reconnect_attempts instead of CONNECT_ATTEMPTS + for attempt in range(max_reconnect_attempts): ws = websocket.WebSocket() try: _connect_websocket(ws, url, timeout=10) @@ -131,8 +156,9 @@ def _ensure_connected(self) -> None: ws.close() except Exception: pass - if attempt < CONNECT_ATTEMPTS - 1: - time.sleep(0.25 * (attempt + 1)) + # ✅ FIXED: Honor the config variable for the attempt boundary too + if attempt < max_reconnect_attempts - 1: + time.sleep(reconnect_interval / 1000.0) if last_error is not None: raise last_error @@ -375,4 +401,4 @@ def close(self) -> None: @property def connected(self) -> bool: """True if the WebSocket is currently connected.""" - return self._ws is not None and not self._closed + return self._ws is not None and not self._closed \ No newline at end of file diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index dd4034b3..238ca07a 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -286,6 +286,13 @@ export interface ExchangeOptions { * built from it lazily. */ signer?: Signer; + + websocket?: { + wsUrl?: string; + reconnectInterval?: number; + pingInterval?: number; + maxReconnectAttempts?: number; + }; } /** @@ -327,6 +334,7 @@ export abstract class Exchange { protected serverManager: ServerManager; protected initPromise: Promise; protected isHosted: boolean; + protected _websocketConfig?: any; /** Wallet address used for hosted endpoints that operate on a wallet. */ public walletAddress?: string; @@ -360,6 +368,7 @@ export abstract class Exchange { this.signatureType = options.signatureType; this.walletAddress = options.walletAddress; this.signer = options.signer; + this._websocketConfig = options.websocket; // Resolve base URL + hosted API key via the shared precedence // rules. See constants.ts for the full resolution table. @@ -575,7 +584,7 @@ export abstract class Exchange { : this.serverManager.getAccessToken(); const authParamName = this.isHosted ? "apiKey" : "token"; - const client = new SidecarWsClient(host, accessToken || undefined, authParamName); + const client = new SidecarWsClient(host, accessToken || undefined, authParamName ,this._websocketConfig ); try { // Trigger connection to validate the endpoint exists. // subscribe() calls ensureConnected internally, but we want @@ -3018,6 +3027,17 @@ export interface PolymarketOptions { /** Optional signature type */ signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number; + + websocket?: { + /** Custom WebSocket endpoint URL (e.g., "wss://custom.example.com/ws") */ + wsUrl?: string; + /** Reconnection delay in milliseconds (default: 5000) */ + reconnectInterval?: number; + /** Heartbeat ping interval in milliseconds (default: 30000) */ + pingInterval?: number; + /** Maximum number of reconnection attempts (default: 10) */ + maxReconnectAttempts?: number; + }; } export class Polymarket extends Exchange { diff --git a/sdks/typescript/pmxt/ws-client.ts b/sdks/typescript/pmxt/ws-client.ts index f7729480..037a10e4 100644 --- a/sdks/typescript/pmxt/ws-client.ts +++ b/sdks/typescript/pmxt/ws-client.ts @@ -42,6 +42,10 @@ export class SidecarWsClient { private authParamName: string; private closed = false; + // Tracking variables for connection resilience + private reconnectAttempts: number = 0; + private pingIntervalId?: any; + /** requestId -> queued data payloads for single-event watch methods */ private dataQueues: Map = new Map(); /** requestId[:symbol] -> latest data payload for batch snapshots */ @@ -52,11 +56,23 @@ export class SidecarWsClient { private activeSubs: Map = new Map(); private connectPromise: Promise | null = null; - - constructor(host: string, accessToken?: string, authParamName: string = "token") { + private config?: { + wsUrl?: string; + reconnectInterval?: number; + pingInterval?: number; + maxReconnectAttempts?: number; + }; + + constructor(host: string, accessToken?: string, authParamName: string = "token", config?: { + wsUrl?: string; + reconnectInterval?: number; + pingInterval?: number; + maxReconnectAttempts?: number; + }) { this.host = host; this.accessToken = accessToken; this.authParamName = authParamName; + this.config = config; } // ------------------------------------------------------------------ @@ -86,11 +102,18 @@ export class SidecarWsClient { hostPart = hostPart.slice("http://".length); } - let url = `${scheme}://${hostPart}/ws`; + let url = this.config?.wsUrl || `${scheme}://${hostPart}/ws`; if (this.accessToken) { - url = `${url}?${this.authParamName}=${this.accessToken}`; + // Check if the URL already contains a query string + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${this.authParamName}=${this.accessToken}`; } + // Extract connection tuning parameters (with fallback defaults) + const reconnectInterval = this.config?.reconnectInterval ?? 5000; + const pingInterval = this.config?.pingInterval ?? 30000; + const maxReconnectAttempts = this.config?.maxReconnectAttempts ?? 10; + // Use the ws package in Node.js, native WebSocket in browsers const WsConstructor = this.getWebSocketConstructor(); if (!WsConstructor) { @@ -103,6 +126,21 @@ export class SidecarWsClient { ws.onopen = () => { this.ws = ws; + this.reconnectAttempts = 0; // Reset attempts upon successful connection + + // Initialize heartbeat if configured + if (pingInterval > 0) { + this.pingIntervalId = setInterval(() => { + if (this.ws && this.ws.readyState === 1 /* WebSocket.OPEN */) { + // Try native ping (Node 'ws' library), fallback to sending a ping frame + if (typeof (this.ws as any).ping === 'function') { + (this.ws as any).ping(); + } else { + this.ws.send(JSON.stringify({ action: 'ping' })); + } + } + }, pingInterval); + } resolve(); }; @@ -120,14 +158,41 @@ export class SidecarWsClient { sub.resolve = null; } } - this.closed = true; + // IMPORTANT: Do NOT set this.closed = true here. + // Let ws.onclose handle the reconnect logic below. this.ws = null; } }; ws.onclose = () => { - this.closed = true; + // Stop the heartbeat to prevent memory leaks + if (this.pingIntervalId) { + clearInterval(this.pingIntervalId); + this.pingIntervalId = undefined; + } this.ws = null; + + // Only attempt reconnect if we haven't maxed out AND we didn't close it intentionally + if (!this.closed && this.reconnectAttempts < maxReconnectAttempts) { + this.reconnectAttempts++; + setTimeout(() => { + // Abort if the user called .close() while we were waiting + if (this.closed) return; + + // Prevent overlapping connectPromises + if (!this.connectPromise) { + this.connectPromise = this.connect(); + this.connectPromise.catch(err => { + logger.debug('[SidecarWsClient] Reconnect attempt failed', { error: String(err) }); + }).finally(() => { + this.connectPromise = null; + }); + } + }, reconnectInterval); + } else if (!this.closed) { + // Max attempts reached, mark as terminal + this.closed = true; + } }; ws.onmessage = (event: any) => { @@ -340,6 +405,10 @@ export class SidecarWsClient { close(): void { this.closed = true; + if (this.pingIntervalId) { + clearInterval(this.pingIntervalId); + this.pingIntervalId = undefined; + } if (this.ws) { try { this.ws.close(); @@ -396,4 +465,4 @@ export class SidecarWsClient { }; }); } -} +} \ No newline at end of file