diff --git a/packages/valory/connections/polymarket_client/connection.py b/packages/valory/connections/polymarket_client/connection.py index 0b18e3cd22..d9f8da06c8 100644 --- a/packages/valory/connections/polymarket_client/connection.py +++ b/packages/valory/connections/polymarket_client/connection.py @@ -89,7 +89,7 @@ "international", ] MARKETS_LIMIT = 300 -EVENTS_LIMIT = 200 +EVENTS_LIMIT = 200`nMAX_PAGES = 100 # Safety cap on the keyset cursor loop (issue #936) MARKETS_TIME_WINDOW_DAYS = 4 API_REQUEST_TIMEOUT = 10 MAX_API_RETRIES = 3 @@ -858,7 +858,7 @@ def _fetch_markets_by_tag_slug( after_cursor: Optional[str] = None all_markets: list = [] - while True: + for _ in range(MAX_PAGES): params: Dict[str, Any] = { "tag_slug": tag_slug, "end_date_max": end_date_max, @@ -875,6 +875,18 @@ def _fetch_markets_by_tag_slug( if error: return None, error + # _request_with_retries can return a non-dict payload (JSON null, a + # stray list, or a string from a misbehaving edge proxy). Calling + # .get() on it would raise AttributeError and bubble up to + # fetch_markets' blanket except Exception, dropping the whole + # category. Degrade gracefully instead. (See issue 936.) + if not isinstance(response, dict): + self.logger.warning( + f"fetch_markets: non-dict response from keyset API " + f"(type={type(response).__name__}); breaking pagination" + ) + break + events_data = response.get("events") or [] markets_this_page = 0 @@ -895,6 +907,14 @@ def _fetch_markets_by_tag_slug( after_cursor = response.get("next_cursor") if not after_cursor: break + else: + # Hit MAX_PAGES without exhausting next_cursor -- upstream may be + # returning the same cursor (server bug, cache poisoning, partial + # outage). Warn so the next refresh can re-attempt. (See issue 936.) + self.logger.warning( + f"fetch_markets: hit MAX_PAGES={MAX_PAGES} cap with non-empty " + f"next_cursor; truncating market list at {len(all_markets)}" + ) return all_markets, None diff --git a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py index 5245c262ba..f9d73d7686 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/polymarket_reedem.py @@ -73,6 +73,14 @@ def _update_policy_for_redeemable_positions( :param redeemable_positions: list of position dicts from the Polymarket API. """ + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return + for position in redeemable_positions: condition_id = position.get("conditionId") if condition_id is None: @@ -262,8 +270,23 @@ def _redeem_via_builder( current_utilized_tools: Optional[str] = None, ) -> Generator: """Redeem positions via builder flow (connection request).""" + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return None # Redeem each position + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return None + for position in redeemable_positions: condition_id = position.get("conditionId") outcome_index = position.get("outcomeIndex") @@ -312,6 +335,14 @@ def _prepare_redeem_tx( return "" # Build redemption transactions and add to multisend_batches + # See issue #970: degrade gracefully on non-list payload. + if not isinstance(redeemable_positions, list): + self.context.logger.error( + f"Expected list of redeemable positions, got " + f"{type(redeemable_positions).__name__}: {redeemable_positions!r}" + ) + return "" + for position in redeemable_positions: condition_id = position.get("conditionId") outcome_index = position.get("outcomeIndex") diff --git a/packages/valory/skills/market_manager_abci/behaviours/base.py b/packages/valory/skills/market_manager_abci/behaviours/base.py index 303cea4774..a80676e1a4 100644 --- a/packages/valory/skills/market_manager_abci/behaviours/base.py +++ b/packages/valory/skills/market_manager_abci/behaviours/base.py @@ -120,6 +120,17 @@ def send_polymarket_connection_request( self.context.logger.warning("No response from the Polymarket connection.") return None + # _request_with_retries can return a non-dict payload (JSON null, a + # list, or a string from a misbehaving edge proxy). Calling + # .get() on it would raise AttributeError and bubble up to + # fetch_markets' blanket except Exception, dropping the whole + # category. Degrade gracefully instead. (See issue #970.) + if response.error: + self.context.logger.warning( + f"Polymarket connection error: {response.error}" + ) + return None + response_json = json.loads(response.payload) return response_json