-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
501 seer add subgraph to subgraphhandler #535
Changes from all commits
fb06daf
38311fc
0506ea0
59bf45a
d9cd35c
43db038
496c159
26a1c3e
0562232
05ad44e
546c840
2d903d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import typing as t | ||
|
||
import tenacity | ||
from pydantic import BaseModel | ||
from subgrounds import FieldPath, Subgrounds | ||
|
||
from prediction_market_agent_tooling.config import APIKeys | ||
from prediction_market_agent_tooling.loggers import logger | ||
from prediction_market_agent_tooling.tools.singleton import SingletonMeta | ||
|
||
T = t.TypeVar("T", bound=BaseModel) | ||
|
||
|
||
class BaseSubgraphHandler(metaclass=SingletonMeta): | ||
def __init__(self) -> None: | ||
self.sg = Subgrounds() | ||
# Patch methods to retry on failure. | ||
self.sg.query_json = tenacity.retry( | ||
stop=tenacity.stop_after_attempt(3), | ||
wait=tenacity.wait_fixed(1), | ||
after=lambda x: logger.debug(f"query_json failed, {x.attempt_number=}."), | ||
)(self.sg.query_json) | ||
self.sg.load_subgraph = tenacity.retry( | ||
stop=tenacity.stop_after_attempt(3), | ||
wait=tenacity.wait_fixed(1), | ||
after=lambda x: logger.debug(f"load_subgraph failed, {x.attempt_number=}."), | ||
)(self.sg.load_subgraph) | ||
|
||
self.keys = APIKeys() | ||
|
||
def _parse_items_from_json( | ||
self, result: list[dict[str, t.Any]] | ||
) -> list[dict[str, t.Any]]: | ||
"""subgrounds return a weird key as a dict key""" | ||
items = [] | ||
for result_chunk in result: | ||
for k, v in result_chunk.items(): | ||
# subgrounds might pack all items as a list, indexed by a key, or pack it as a dictionary (if one single element) | ||
if v is None: | ||
continue | ||
elif isinstance(v, dict): | ||
items.extend([v]) | ||
else: | ||
items.extend(v) | ||
return items | ||
|
||
def do_query(self, fields: list[FieldPath], pydantic_model: t.Type[T]) -> list[T]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
result = self.sg.query_json(fields) | ||
items = self._parse_items_from_json(result) | ||
models = [pydantic_model.model_validate(i) for i in items] | ||
return models |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from pydantic import BaseModel, ConfigDict, Field | ||
|
||
from prediction_market_agent_tooling.gtypes import HexBytes | ||
|
||
|
||
class SeerMarket(BaseModel): | ||
model_config = ConfigDict(populate_by_name=True) | ||
|
||
id: HexBytes | ||
title: str = Field(alias="marketName") | ||
outcomes: list[str] | ||
parent_market: HexBytes = Field(alias="parentMarket") | ||
wrapped_tokens: list[HexBytes] = Field(alias="wrappedTokens") | ||
|
||
|
||
class SeerToken(BaseModel): | ||
id: HexBytes | ||
name: str | ||
symbol: str | ||
|
||
|
||
class SeerPool(BaseModel): | ||
model_config = ConfigDict(populate_by_name=True) | ||
id: HexBytes | ||
liquidity: int | ||
token0: SeerToken | ||
token1: SeerToken |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,142 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from subgrounds import FieldPath | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from web3.constants import ADDRESS_ZERO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from prediction_market_agent_tooling.markets.base_subgraph_handler import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BaseSubgraphHandler, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from prediction_market_agent_tooling.markets.seer.data_models import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
SeerMarket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
SeerPool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
INVALID_OUTCOME = "Invalid result" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class SeerSubgraphHandler(BaseSubgraphHandler): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Class responsible for handling interactions with Seer subgraphs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
SEER_SUBGRAPH = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/B4vyRqJaSHD8dRDb3BFRoAzuBK18c1QQcXq94JbxDxWH" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
SWAPR_ALGEBRA_SUBGRAPH = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/AAA1vYjxwFHzbt6qKwLHNcDSASyr1J1xVViDH8gTMFMR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
INVALID_ANSWER = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
gabrielfior marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def __init__(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super().__init__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.seer_subgraph = self.sg.load_subgraph( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.SEER_SUBGRAPH.format( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
graph_api_key=self.keys.graph_api_key.get_secret_value() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.swapr_algebra_subgraph = self.sg.load_subgraph( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.SWAPR_ALGEBRA_SUBGRAPH.format( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
graph_api_key=self.keys.graph_api_key.get_secret_value() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+29
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for subgraph loading failures Currently, if the subgraphs fail to load due to an invalid Apply this change to add error handling: def __init__(self) -> None:
super().__init__()
try:
self.seer_subgraph = self.sg.load_subgraph(
self.SEER_SUBGRAPH.format(
graph_api_key=self.keys.graph_api_key.get_secret_value()
)
)
self.swapr_algebra_subgraph = self.sg.load_subgraph(
self.SWAPR_ALGEBRA_SUBGRAPH.format(
graph_api_key=self.keys.graph_api_key.get_secret_value()
)
)
except Exception as e:
# Handle the exception or log an error message
raise ConnectionError(f"Failed to load subgraphs: {e}") |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def _get_fields_for_markets(self, markets_field: FieldPath) -> list[FieldPath]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.factory, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.creator, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.marketName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.outcomes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.parentMarket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.finalizeTs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field.wrappedTokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def filter_bicategorical_markets(markets: list[SeerMarket]) -> list[SeerMarket]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# We do an extra check for the invalid outcome for safety. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
m for m in markets if len(m.outcomes) == 3 and INVALID_OUTCOME in m.outcomes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def filter_binary_markets(markets: list[SeerMarket]) -> list[SeerMarket]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
market | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for market in markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if {"yes", "no"}.issubset({o.lower() for o in market.outcomes}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def build_filter_for_conditional_markets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
include_conditional_markets: bool = True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) -> dict[Any, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if include_conditional_markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
else {"parentMarket": ADDRESS_ZERO.lower()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_bicategorical_markets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self, include_conditional_markets: bool = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) -> list[SeerMarket]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Returns markets that contain 2 categories plus an invalid outcome.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
query_filter = self.build_filter_for_conditional_markets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
include_conditional_markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
query_filter["outcomes_contains"] = [INVALID_OUTCOME] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field = self.seer_subgraph.Query.markets(where=query_filter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields = self._get_fields_for_markets(markets_field) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets = self.do_query(fields=fields, pydantic_model=SeerMarket) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
two_category_markets = self.filter_bicategorical_markets(markets) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return two_category_markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_binary_markets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self, include_conditional_markets: bool = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) -> list[SeerMarket]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
two_category_markets = self.get_bicategorical_markets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
include_conditional_markets=include_conditional_markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Now we additionally filter markets based on YES/NO being the only outcomes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
binary_markets = self.filter_binary_markets(two_category_markets) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return binary_markets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
gabrielfior marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_market_by_id(self, market_id: HexBytes) -> SeerMarket: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets_field = self.seer_subgraph.Query.market(id=market_id.hex().lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields = self._get_fields_for_markets(markets_field) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
markets = self.do_query(fields=fields, pydantic_model=SeerMarket) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if len(markets) != 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
f"Fetched wrong number of markets. Expected 1 but got {len(markets)}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+110
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include Including the Apply this diff to improve the error message: if len(markets) != 1:
raise ValueError(
- f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
+ f"Fetched wrong number of markets for ID {market_id.hex()}. Expected 1 but got {len(markets)}"
) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return markets[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.liquidity, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token0.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token0.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token0.symbol, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token1.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token1.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field.token1.symbol, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_pools_for_market(self, market: SeerMarket) -> list[SeerPool]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
wheres = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for wrapped_token in market.wrapped_tokens: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
wheres.extend( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{"token0": wrapped_token.hex().lower()}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{"token1": wrapped_token.hex().lower()}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools_field = self.swapr_algebra_subgraph.Query.pools(where={"or": wheres}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields = self._get_fields_for_pools(pools_field) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pools = self.do_query(fields=fields, pydantic_model=SeerPool) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return pools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+129
to
+142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation for market parameter. The method should validate that the input market is not None before proceeding. def get_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
+ if market is None:
+ raise ValueError("Market cannot be None")
+
# We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
wheres = [] 📝 Committable suggestion
Suggested change
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename unused variable
k
to_k
The variable
k
in the loop is not used within the loop body. Renaming it to_k
indicates that it is intentionally unused and follows Python conventions.Apply this diff to fix the issue:
📝 Committable suggestion
🧰 Tools
🪛 Ruff
40-40: Loop control variable
k
not used within loop bodyRename unused
k
to_k
(B007)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Common that's too big neat picking! 😄 And if anything one could just use
result_chunk.values()
to not have the extra variable at all!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kongzii, you're absolutely right! Using
result_chunk.values()
would simplify the code and eliminate the need for the unused variable altogether. Thank you for pointing that out!🐰🔍
✏️ Learnings added