prediction-market-agent-tooling 0.67.2__py3-none-any.whl → 0.67.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
  2. prediction_market_agent_tooling/deploy/agent.py +57 -51
  3. prediction_market_agent_tooling/deploy/betting_strategy.py +1 -1
  4. prediction_market_agent_tooling/markets/agent_market.py +7 -1
  5. prediction_market_agent_tooling/markets/manifold/manifold.py +2 -1
  6. prediction_market_agent_tooling/markets/metaculus/metaculus.py +2 -1
  7. prediction_market_agent_tooling/markets/omen/omen.py +2 -1
  8. prediction_market_agent_tooling/markets/polymarket/polymarket.py +2 -1
  9. prediction_market_agent_tooling/markets/seer/data_models.py +25 -1
  10. prediction_market_agent_tooling/markets/seer/seer.py +74 -24
  11. prediction_market_agent_tooling/markets/seer/seer_contracts.py +18 -0
  12. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +122 -18
  13. prediction_market_agent_tooling/tools/contract.py +59 -0
  14. prediction_market_agent_tooling/tools/cow/cow_order.py +4 -1
  15. prediction_market_agent_tooling/tools/rephrase.py +1 -1
  16. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +57 -0
  17. {prediction_market_agent_tooling-0.67.2.dist-info → prediction_market_agent_tooling-0.67.3.dist-info}/METADATA +1 -1
  18. {prediction_market_agent_tooling-0.67.2.dist-info → prediction_market_agent_tooling-0.67.3.dist-info}/RECORD +21 -20
  19. {prediction_market_agent_tooling-0.67.2.dist-info → prediction_market_agent_tooling-0.67.3.dist-info}/LICENSE +0 -0
  20. {prediction_market_agent_tooling-0.67.2.dist-info → prediction_market_agent_tooling-0.67.3.dist-info}/WHEEL +0 -0
  21. {prediction_market_agent_tooling-0.67.2.dist-info → prediction_market_agent_tooling-0.67.3.dist-info}/entry_points.txt +0 -0
@@ -48,10 +48,14 @@ class CreateCategoricalMarketsParams(BaseModel):
48
48
  SEER_BASE_URL = "https://app.seer.pm"
49
49
 
50
50
 
51
- def seer_normalize_wei(value: int | None) -> int | None:
51
+ def seer_normalize_wei(value: int | dict[str, t.Any] | None) -> int | None:
52
52
  # See https://github.com/seer-pm/demo/blob/main/web/netlify/edge-functions/utils/common.ts#L22
53
53
  if value is None:
54
54
  return value
55
+ elif isinstance(value, dict):
56
+ if value.get("value") is None:
57
+ raise ValueError(f"Expected a dictionary with a value key, but got {value}")
58
+ value = int(value["value"])
55
59
  is_in_wei = value > 1e10
56
60
  return value if is_in_wei else value * 10**18
57
61
 
@@ -59,6 +63,21 @@ def seer_normalize_wei(value: int | None) -> int | None:
59
63
  SeerNormalizedWei = Annotated[Wei | None, BeforeValidator(seer_normalize_wei)]
60
64
 
61
65
 
66
+ class MarketId(BaseModel):
67
+ id: HexBytes
68
+
69
+
70
+ class SeerQuestion(BaseModel):
71
+ id: str
72
+ best_answer: HexBytes
73
+ finalize_ts: int
74
+
75
+
76
+ class SeerMarketQuestions(BaseModel):
77
+ question: SeerQuestion
78
+ market: MarketId
79
+
80
+
62
81
  class SeerMarket(BaseModel):
63
82
  model_config = ConfigDict(populate_by_name=True)
64
83
 
@@ -71,6 +90,7 @@ class SeerMarket(BaseModel):
71
90
  alias="parentOutcome", description="It comes as 0 from non-conditioned markets."
72
91
  )
73
92
  parent_market: t.Optional["SeerMarket"] = Field(alias="parentMarket", default=None)
93
+ template_id: int = Field(alias="templateId")
74
94
  collateral_token: HexAddress = Field(alias="collateralToken")
75
95
  condition_id: HexBytes = Field(alias="conditionId")
76
96
  opening_ts: int = Field(alias="openingTs")
@@ -143,6 +163,10 @@ class SeerMarket(BaseModel):
143
163
  return urljoin(SEER_BASE_URL, f"markets/{chain_id}/{self.id.hex()}")
144
164
 
145
165
 
166
+ class SeerMarketWithQuestions(SeerMarket):
167
+ questions: list[SeerMarketQuestions]
168
+
169
+
146
170
  class RedeemParams(BaseModel):
147
171
  model_config = ConfigDict(populate_by_name=True)
148
172
  market: ChecksumAddress
@@ -23,8 +23,8 @@ from prediction_market_agent_tooling.gtypes import (
23
23
  from prediction_market_agent_tooling.loggers import logger
24
24
  from prediction_market_agent_tooling.markets.agent_market import (
25
25
  AgentMarket,
26
+ ConditionalFilterType,
26
27
  FilterBy,
27
- MarketFees,
28
28
  ParentMarket,
29
29
  ProcessedMarket,
30
30
  ProcessedTradedMarket,
@@ -32,7 +32,10 @@ from prediction_market_agent_tooling.markets.agent_market import (
32
32
  SortBy,
33
33
  )
34
34
  from prediction_market_agent_tooling.markets.blockchain_utils import store_trades
35
- from prediction_market_agent_tooling.markets.data_models import ExistingPosition
35
+ from prediction_market_agent_tooling.markets.data_models import (
36
+ ExistingPosition,
37
+ Resolution,
38
+ )
36
39
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
37
40
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
38
41
  from prediction_market_agent_tooling.markets.omen.omen_constants import (
@@ -44,6 +47,7 @@ from prediction_market_agent_tooling.markets.omen.omen_contracts import (
44
47
  from prediction_market_agent_tooling.markets.seer.data_models import (
45
48
  RedeemParams,
46
49
  SeerMarket,
50
+ SeerMarketWithQuestions,
47
51
  )
48
52
  from prediction_market_agent_tooling.markets.seer.exceptions import (
49
53
  PriceCalculationError,
@@ -54,7 +58,9 @@ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
54
58
  SeerMarketFactory,
55
59
  )
56
60
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
61
+ SeerQuestionsCache,
57
62
  SeerSubgraphHandler,
63
+ TemplateId,
58
64
  )
59
65
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
60
66
  NewMarketEvent,
@@ -257,7 +263,7 @@ class SeerAgentMarket(AgentMarket):
257
263
  @staticmethod
258
264
  def _filter_markets_contained_in_trades(
259
265
  api_keys: APIKeys,
260
- markets: list[SeerMarket],
266
+ markets: t.Sequence[SeerMarket],
261
267
  ) -> list[SeerMarket]:
262
268
  """
263
269
  We filter the markets using previous trades by the user so that we don't have to process all Seer markets.
@@ -267,7 +273,7 @@ class SeerAgentMarket(AgentMarket):
267
273
  traded_tokens = {t.buyToken for t in trades_by_user}.union(
268
274
  [t.sellToken for t in trades_by_user]
269
275
  )
270
- filtered_markets = []
276
+ filtered_markets: list[SeerMarket] = []
271
277
  for market in markets:
272
278
  if any(
273
279
  [
@@ -338,9 +344,64 @@ class SeerAgentMarket(AgentMarket):
338
344
  return OmenAgentMarket.verify_operational_balance(api_keys=api_keys)
339
345
 
340
346
  @staticmethod
341
- def from_data_model_with_subgraph(
347
+ def build_resolution(
348
+ model: SeerMarketWithQuestions,
349
+ ) -> Resolution | None:
350
+ if model.questions[0].question.finalize_ts == 0:
351
+ # resolution not yet finalized
352
+ return None
353
+
354
+ if model.template_id != TemplateId.CATEGORICAL:
355
+ logger.warning("Resolution can only be built for categorical markets.")
356
+ # Future note - for scalar markets, simply fetch best_answer and convert
357
+ # from hex into int and divide by 1e18 (because Wei).
358
+ return None
359
+
360
+ if len(model.questions) != 1:
361
+ raise ValueError("Seer categorical markets must have 1 question.")
362
+
363
+ question = model.questions[0]
364
+ outcome = model.outcomes[int(question.question.best_answer.hex(), 16)]
365
+ return Resolution(outcome=outcome, invalid=False)
366
+
367
+ @staticmethod
368
+ def convert_seer_market_into_market_with_questions(
369
+ seer_market: SeerMarket, seer_subgraph: SeerSubgraphHandler
370
+ ) -> "SeerMarketWithQuestions":
371
+ q = SeerQuestionsCache(seer_subgraph_handler=seer_subgraph)
372
+ q.fetch_questions([seer_market.id])
373
+ questions = q.market_id_to_questions[seer_market.id]
374
+ return SeerMarketWithQuestions(**seer_market.model_dump(), questions=questions)
375
+
376
+ @staticmethod
377
+ def get_parent(
342
378
  model: SeerMarket,
343
379
  seer_subgraph: SeerSubgraphHandler,
380
+ ) -> t.Optional["ParentMarket"]:
381
+ if not model.parent_market:
382
+ return None
383
+
384
+ # turn into a market with questions
385
+ parent_market_with_questions = (
386
+ SeerAgentMarket.convert_seer_market_into_market_with_questions(
387
+ model.parent_market, seer_subgraph=seer_subgraph
388
+ )
389
+ )
390
+
391
+ market_with_questions = check_not_none(
392
+ SeerAgentMarket.from_data_model_with_subgraph(
393
+ parent_market_with_questions, seer_subgraph, False
394
+ )
395
+ )
396
+
397
+ return ParentMarket(
398
+ market=market_with_questions, parent_outcome=model.parent_outcome
399
+ )
400
+
401
+ @staticmethod
402
+ def from_data_model_with_subgraph(
403
+ model: SeerMarketWithQuestions,
404
+ seer_subgraph: SeerSubgraphHandler,
344
405
  must_have_prices: bool,
345
406
  ) -> t.Optional["SeerAgentMarket"]:
346
407
  price_manager = PriceManager(seer_market=model, seer_subgraph=seer_subgraph)
@@ -356,6 +417,10 @@ class SeerAgentMarket(AgentMarket):
356
417
  # Price calculation failed, so don't return the market
357
418
  return None
358
419
 
420
+ resolution = SeerAgentMarket.build_resolution(model=model)
421
+
422
+ parent = SeerAgentMarket.get_parent(model=model, seer_subgraph=seer_subgraph)
423
+
359
424
  market = SeerAgentMarket(
360
425
  id=model.id.hex(),
361
426
  question=model.title,
@@ -370,27 +435,12 @@ class SeerAgentMarket(AgentMarket):
370
435
  fees=MarketFees.get_zero_fees(),
371
436
  outcome_token_pool=None,
372
437
  outcomes_supply=model.outcomes_supply,
373
- resolution=None,
438
+ resolution=resolution,
374
439
  volume=None,
375
440
  probabilities=probability_map,
376
441
  upper_bound=model.upper_bound,
377
442
  lower_bound=model.lower_bound,
378
- parent=(
379
- ParentMarket(
380
- market=(
381
- check_not_none(
382
- SeerAgentMarket.from_data_model_with_subgraph(
383
- model.parent_market,
384
- seer_subgraph,
385
- False,
386
- )
387
- )
388
- ),
389
- parent_outcome=model.parent_outcome,
390
- )
391
- if model.parent_market
392
- else None
393
- ),
443
+ parent=parent,
394
444
  )
395
445
 
396
446
  return market
@@ -403,7 +453,7 @@ class SeerAgentMarket(AgentMarket):
403
453
  created_after: t.Optional[DatetimeUTC] = None,
404
454
  excluded_questions: set[str] | None = None,
405
455
  question_type: QuestionType = QuestionType.ALL,
406
- include_conditional_markets: bool = False,
456
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
407
457
  ) -> t.Sequence["SeerAgentMarket"]:
408
458
  seer_subgraph = SeerSubgraphHandler()
409
459
 
@@ -412,7 +462,7 @@ class SeerAgentMarket(AgentMarket):
412
462
  sort_by=sort_by,
413
463
  filter_by=filter_by,
414
464
  question_type=question_type,
415
- include_conditional_markets=include_conditional_markets,
465
+ conditional_filter_type=conditional_filter_type,
416
466
  )
417
467
 
418
468
  # We exclude the None values below because `from_data_model_with_subgraph` can return None, which
@@ -9,6 +9,7 @@ from prediction_market_agent_tooling.gtypes import (
9
9
  ChecksumAddress,
10
10
  OutcomeStr,
11
11
  TxReceipt,
12
+ Wei,
12
13
  xDai,
13
14
  )
14
15
  from prediction_market_agent_tooling.markets.seer.data_models import (
@@ -115,6 +116,23 @@ class GnosisRouter(ContractOnGnosisChain):
115
116
  )
116
117
  return receipt_tx
117
118
 
119
+ def split_position(
120
+ self,
121
+ api_keys: APIKeys,
122
+ collateral_token: ChecksumAddress,
123
+ market_id: ChecksumAddress,
124
+ amount: Wei,
125
+ web3: Web3 | None = None,
126
+ ) -> TxReceipt:
127
+ """Splits collateral token into full set of outcome tokens."""
128
+ receipt_tx = self.send(
129
+ api_keys=api_keys,
130
+ function_name="splitPosition",
131
+ function_params=[collateral_token, market_id, amount],
132
+ web3=web3,
133
+ )
134
+ return receipt_tx
135
+
118
136
 
119
137
  class SwaprRouterContract(ContractOnGnosisChain):
120
138
  # File content taken from https://github.com/protofire/omen-exchange/blob/master/app/src/abi/marketMaker.json.
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  import typing as t
3
+ from collections import defaultdict
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
@@ -15,6 +16,7 @@ from prediction_market_agent_tooling.deploy.constants import (
15
16
  from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
16
17
  from prediction_market_agent_tooling.loggers import logger
17
18
  from prediction_market_agent_tooling.markets.agent_market import (
19
+ ConditionalFilterType,
18
20
  FilterBy,
19
21
  QuestionType,
20
22
  SortBy,
@@ -22,9 +24,14 @@ from prediction_market_agent_tooling.markets.agent_market import (
22
24
  from prediction_market_agent_tooling.markets.base_subgraph_handler import (
23
25
  BaseSubgraphHandler,
24
26
  )
25
- from prediction_market_agent_tooling.markets.seer.data_models import SeerMarket
27
+ from prediction_market_agent_tooling.markets.seer.data_models import (
28
+ SeerMarket,
29
+ SeerMarketQuestions,
30
+ SeerMarketWithQuestions,
31
+ )
26
32
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import SeerPool
27
33
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
34
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
28
35
  from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
29
36
  from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
30
37
 
@@ -85,12 +92,6 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
85
92
  markets_field.upperBound,
86
93
  markets_field.lowerBound,
87
94
  markets_field.templateId,
88
- # TODO: On the Subgraph, `questions` field is a kind of sub-query, instead of a classic list of values.
89
- # See how it is shown on their UI: https://thegraph.com/explorer/subgraphs/B4vyRqJaSHD8dRDb3BFRoAzuBK18c1QQcXq94JbxDxWH?view=Query&chain=arbitrum-one.
90
- # And that doesn't work with subgrounds.
91
- # markets_field.questions.question.id,
92
- # markets_field.questions.question.finalize_ts,
93
- # markets_field.questions.question.best_answer,
94
95
  ]
95
96
  if current_level < max_level:
96
97
  fields.extend(
@@ -132,8 +133,8 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
132
133
  def _build_where_statements(
133
134
  filter_by: FilterBy,
134
135
  outcome_supply_gt_if_open: Wei,
135
- include_conditional_markets: bool = False,
136
136
  question_type: QuestionType = QuestionType.ALL,
137
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
137
138
  parent_market_id: HexBytes | None = None,
138
139
  ) -> dict[Any, Any]:
139
140
  now = to_int_timestamp(utcnow())
@@ -153,9 +154,6 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
153
154
  case _:
154
155
  raise ValueError(f"Unknown filter {filter_by}")
155
156
 
156
- if not include_conditional_markets:
157
- and_stms["parentMarket"] = ADDRESS_ZERO.lower()
158
-
159
157
  if parent_market_id:
160
158
  and_stms["parentMarket"] = parent_market_id.hex().lower()
161
159
 
@@ -196,9 +194,21 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
196
194
  }
197
195
  )
198
196
 
199
- # If none specified, don't add any template/outcome filters (returns all types)
197
+ # Build filters for conditional_filter type
198
+ conditional_filter = {}
199
+ match conditional_filter_type:
200
+ case ConditionalFilterType.ONLY_CONDITIONAL:
201
+ conditional_filter["parentMarket_not"] = ADDRESS_ZERO.lower()
202
+ case ConditionalFilterType.ONLY_NOT_CONDITIONAL:
203
+ conditional_filter["parentMarket"] = ADDRESS_ZERO.lower()
204
+ case ConditionalFilterType.ALL:
205
+ pass
206
+ case _:
207
+ raise ValueError(
208
+ f"Unknown conditional filter {conditional_filter_type}"
209
+ )
200
210
 
201
- all_filters = [and_stms] + outcome_filters if and_stms else outcome_filters
211
+ all_filters = outcome_filters + [and_stms, conditional_filter]
202
212
  where_stms: dict[str, t.Any] = {"and": all_filters}
203
213
  return where_stms
204
214
 
@@ -235,9 +245,9 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
235
245
  limit: int | None = None,
236
246
  outcome_supply_gt_if_open: Wei = Wei(0),
237
247
  question_type: QuestionType = QuestionType.ALL,
238
- include_conditional_markets: bool = False,
248
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
239
249
  parent_market_id: HexBytes | None = None,
240
- ) -> list[SeerMarket]:
250
+ ) -> list[SeerMarketWithQuestions]:
241
251
  sort_direction, sort_by_field = self._build_sort_params(sort_by)
242
252
 
243
253
  where_stms = self._build_where_statements(
@@ -245,7 +255,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
245
255
  outcome_supply_gt_if_open=outcome_supply_gt_if_open,
246
256
  parent_market_id=parent_market_id,
247
257
  question_type=question_type,
248
- include_conditional_markets=include_conditional_markets,
258
+ conditional_filter_type=conditional_filter_type,
249
259
  )
250
260
 
251
261
  # These values can not be set to `None`, but they can be omitted.
@@ -264,12 +274,65 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
264
274
  )
265
275
  fields = self._get_fields_for_markets(markets_field)
266
276
  markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
267
- return markets
277
+ market_ids = [m.id for m in markets]
278
+ # We fetch questions from all markets and all parents in one go
279
+ parent_market_ids = [
280
+ m.parent_market.id for m in markets if m.parent_market is not None
281
+ ]
282
+ q = SeerQuestionsCache(seer_subgraph_handler=self)
283
+ q.fetch_questions(list(set(market_ids + parent_market_ids)))
284
+
285
+ # Create SeerMarketWithQuestions for each market
286
+ return [
287
+ SeerMarketWithQuestions(
288
+ **m.model_dump(), questions=q.market_id_to_questions[m.id]
289
+ )
290
+ for m in markets
291
+ ]
268
292
 
269
- def get_market_by_id(self, market_id: HexBytes) -> SeerMarket:
293
+ def get_questions_for_markets(
294
+ self, market_ids: list[HexBytes]
295
+ ) -> list[SeerMarketQuestions]:
296
+ where = unwrap_generic_value(
297
+ {"market_in": [market_id.hex().lower() for market_id in market_ids]}
298
+ )
299
+ markets_field = self.seer_subgraph.Query.marketQuestions(where=where)
300
+ fields = self._get_fields_for_questions(markets_field)
301
+ questions = self.do_query(fields=fields, pydantic_model=SeerMarketQuestions)
302
+ return questions
303
+
304
+ def get_market_by_id(self, market_id: HexBytes) -> SeerMarketWithQuestions:
270
305
  markets_field = self.seer_subgraph.Query.market(id=market_id.hex().lower())
271
306
  fields = self._get_fields_for_markets(markets_field)
272
307
  markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
308
+ if len(markets) != 1:
309
+ raise ValueError(
310
+ f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
311
+ )
312
+ q = SeerQuestionsCache(self)
313
+ q.fetch_questions([market_id])
314
+ questions = q.market_id_to_questions[market_id]
315
+ s = SeerMarketWithQuestions.model_validate(
316
+ markets[0].model_dump() | {"questions": questions}
317
+ )
318
+ return s
319
+
320
+ def _get_fields_for_questions(self, questions_field: FieldPath) -> list[FieldPath]:
321
+ fields = [
322
+ questions_field.question.id,
323
+ questions_field.question.best_answer,
324
+ questions_field.question.finalize_ts,
325
+ questions_field.market.id,
326
+ ]
327
+ return fields
328
+
329
+ def get_market_by_wrapped_token(self, token: ChecksumAddress) -> SeerMarket:
330
+ where_stms = {"wrappedTokens_contains": [token]}
331
+ markets_field = self.seer_subgraph.Query.markets(
332
+ where=unwrap_generic_value(where_stms)
333
+ )
334
+ fields = self._get_fields_for_markets(markets_field)
335
+ markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
273
336
  if len(markets) != 1:
274
337
  raise ValueError(
275
338
  f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
@@ -328,3 +391,44 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
328
391
  # We select the first one
329
392
  return pools[0]
330
393
  return None
394
+
395
+
396
+ class SeerQuestionsCache(metaclass=SingletonMeta):
397
+ """A singleton cache for storing and retrieving Seer market questions.
398
+
399
+ This class provides an in-memory cache for Seer market questions, preventing
400
+ redundant subgraph queries by maintaining a mapping of market IDs to their
401
+ associated questions. It implements the singleton pattern to ensure a single
402
+ cache instance is used throughout the agent run.
403
+
404
+ Attributes:
405
+ market_id_to_questions: A dictionary mapping market IDs to lists of SeerMarketQuestions
406
+ seer_subgraph_handler: Handler for interacting with the Seer subgraph
407
+ """
408
+
409
+ def __init__(self, seer_subgraph_handler: SeerSubgraphHandler | None = None):
410
+ self.market_id_to_questions: dict[
411
+ HexBytes, list[SeerMarketQuestions]
412
+ ] = defaultdict(list)
413
+ self.seer_subgraph_handler = seer_subgraph_handler or SeerSubgraphHandler()
414
+
415
+ def fetch_questions(self, market_ids: list[HexBytes]) -> None:
416
+ filtered_list = [
417
+ market_id
418
+ for market_id in market_ids
419
+ if market_id not in self.market_id_to_questions
420
+ ]
421
+ if not filtered_list:
422
+ return
423
+
424
+ questions = self.seer_subgraph_handler.get_questions_for_markets(filtered_list)
425
+ # Group questions by market_id
426
+ questions_by_market: dict[HexBytes, list[SeerMarketQuestions]] = defaultdict(
427
+ list
428
+ )
429
+ for q in questions:
430
+ questions_by_market[q.market.id].append(q)
431
+
432
+ # Update the cache with the new questions for each market
433
+ for market_id, market_questions in questions_by_market.items():
434
+ self.market_id_to_questions[market_id] = market_questions
@@ -413,6 +413,20 @@ class ContractERC4626BaseClass(ContractERC20BaseClass):
413
413
  return self.convertToShares(amount, web3=web3)
414
414
 
415
415
 
416
+ class ContractWrapped1155BaseClass(ContractERC20BaseClass):
417
+ """Wrapped 1155 contract from Seer (https://gnosisscan.io/address/0x2f9c49974ad8b9b31424d9dc812667b16310ca50#readContract)
418
+ Source code - https://github.com/seer-pm/demo/blob/main/contracts/src/interaction/1155-to-20/Wrapped1155Factory.sol#L224
419
+ This contract inherits from ERC20 and contains additional properties, such as multiToken (conditional Token contract implementation)
420
+ and tokenId (token identifier). Goal is to wrap individual tokens into a standalone ERC20 token.
421
+ """
422
+
423
+ abi: ABI = abi_field_validator(
424
+ os.path.join(
425
+ os.path.dirname(os.path.realpath(__file__)), "../abis/erc1155.abi.json"
426
+ )
427
+ )
428
+
429
+
416
430
  class OwnableContract(ContractBaseClass):
417
431
  abi: ABI = abi_field_validator(
418
432
  os.path.join(
@@ -533,6 +547,12 @@ class ContractDepositableWrapperERC20OnGnosisChain(
533
547
  """
534
548
 
535
549
 
550
+ class ContractWrapped1155OnGnosisChain(
551
+ ContractWrapped1155BaseClass, ContractERC20OnGnosisChain
552
+ ):
553
+ pass
554
+
555
+
536
556
  class ContractERC4626OnGnosisChain(
537
557
  ContractERC4626BaseClass, ContractERC20OnGnosisChain
538
558
  ):
@@ -626,6 +646,11 @@ def contract_implements_function(
626
646
  function_name=function_name,
627
647
  web3=web3,
628
648
  function_arg_types=function_arg_types,
649
+ ) or seer_minimal_proxy_implements_function(
650
+ contract_address=contract_address,
651
+ function_name=function_name,
652
+ web3=web3,
653
+ function_arg_types=function_arg_types,
629
654
  )
630
655
 
631
656
  return implements
@@ -654,6 +679,31 @@ def minimal_proxy_implements_function(
654
679
  return False
655
680
 
656
681
 
682
+ def seer_minimal_proxy_implements_function(
683
+ contract_address: ChecksumAddress,
684
+ function_name: str,
685
+ web3: Web3,
686
+ function_arg_types: list[str] | None = None,
687
+ ) -> bool:
688
+ try:
689
+ # Read address between specific indices to find logic contract
690
+ bytecode = web3.eth.get_code(contract_address)
691
+ logic_contract_address = bytecode[11:31]
692
+ if not Web3.is_address(logic_contract_address):
693
+ return False
694
+
695
+ return contract_implements_function(
696
+ Web3.to_checksum_address(logic_contract_address),
697
+ function_name=function_name,
698
+ web3=web3,
699
+ function_arg_types=function_arg_types,
700
+ look_for_proxy_contract=False,
701
+ )
702
+ except DecodingError:
703
+ logger.info("Error decoding contract address on seer minimal proxy")
704
+ return False
705
+
706
+
657
707
  def init_collateral_token_contract(
658
708
  address: ChecksumAddress, web3: Web3 | None
659
709
  ) -> ContractERC20BaseClass:
@@ -673,6 +723,13 @@ def init_collateral_token_contract(
673
723
  ):
674
724
  return ContractDepositableWrapperERC20BaseClass(address=address)
675
725
 
726
+ elif contract_implements_function(
727
+ address,
728
+ "multiToken",
729
+ web3=web3,
730
+ ):
731
+ return ContractWrapped1155BaseClass(address=address)
732
+
676
733
  elif contract_implements_function(
677
734
  address,
678
735
  "balanceOf",
@@ -694,6 +751,8 @@ def to_gnosis_chain_contract(
694
751
  return ContractERC4626OnGnosisChain(address=contract.address)
695
752
  elif isinstance(contract, ContractDepositableWrapperERC20BaseClass):
696
753
  return ContractDepositableWrapperERC20OnGnosisChain(address=contract.address)
754
+ elif isinstance(contract, ContractWrapped1155BaseClass):
755
+ return ContractWrapped1155OnGnosisChain(address=contract.address)
697
756
  elif isinstance(contract, ContractERC20BaseClass):
698
757
  return ContractERC20OnGnosisChain(address=contract.address)
699
758
  else:
@@ -167,10 +167,13 @@ def handle_allowance(
167
167
  api_keys: APIKeys,
168
168
  sell_token: ChecksumAddress,
169
169
  amount_wei: Wei,
170
+ for_address: ChecksumAddress | None = None,
170
171
  web3: Web3 | None = None,
171
172
  ) -> None:
172
173
  # Approve the CoW Swap Vault Relayer to get the sell token only if allowance not sufficient.
173
- for_address = Web3.to_checksum_address(CowContractAddress.VAULT_RELAYER.value)
174
+ for_address = for_address or Web3.to_checksum_address(
175
+ CowContractAddress.VAULT_RELAYER.value
176
+ )
174
177
  current_allowance = ContractERC20OnGnosisChain(address=sell_token).allowance(
175
178
  owner=api_keys.bet_from_address,
176
179
  for_address=for_address,
@@ -33,7 +33,7 @@ Output only the rephrased question.
33
33
  @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_fixed(1))
34
34
  @observe()
35
35
  @db_cache
36
- def rephrase_question_to_unconditioned(
36
+ def rephrase_question_to_unconditional(
37
37
  question: str,
38
38
  parent_question: str,
39
39
  needed_parent_outcome: str,