prediction-market-agent-tooling 0.68.0.dev999__py3-none-any.whl → 0.69.0__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 (42) hide show
  1. prediction_market_agent_tooling/chains.py +1 -0
  2. prediction_market_agent_tooling/config.py +37 -2
  3. prediction_market_agent_tooling/deploy/agent.py +26 -21
  4. prediction_market_agent_tooling/deploy/betting_strategy.py +133 -22
  5. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  6. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +17 -20
  7. prediction_market_agent_tooling/markets/agent_market.py +27 -9
  8. prediction_market_agent_tooling/markets/blockchain_utils.py +3 -3
  9. prediction_market_agent_tooling/markets/markets.py +16 -0
  10. prediction_market_agent_tooling/markets/omen/data_models.py +3 -18
  11. prediction_market_agent_tooling/markets/omen/omen.py +26 -11
  12. prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -196
  13. prediction_market_agent_tooling/markets/omen/omen_resolving.py +2 -2
  14. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +13 -11
  15. prediction_market_agent_tooling/markets/polymarket/api.py +35 -1
  16. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  17. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  18. prediction_market_agent_tooling/markets/polymarket/data_models.py +33 -5
  19. prediction_market_agent_tooling/markets/polymarket/polymarket.py +247 -18
  20. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  21. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +2 -1
  22. prediction_market_agent_tooling/markets/seer/data_models.py +41 -6
  23. prediction_market_agent_tooling/markets/seer/price_manager.py +69 -1
  24. prediction_market_agent_tooling/markets/seer/seer.py +77 -26
  25. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  26. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +71 -20
  27. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +67 -0
  28. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +17 -22
  29. prediction_market_agent_tooling/tools/contract.py +236 -4
  30. prediction_market_agent_tooling/tools/cow/cow_order.py +13 -8
  31. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  32. prediction_market_agent_tooling/tools/hexbytes_custom.py +3 -9
  33. prediction_market_agent_tooling/tools/langfuse_client_utils.py +17 -5
  34. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +2 -2
  35. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  36. prediction_market_agent_tooling/tools/web3_utils.py +9 -4
  37. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/METADATA +8 -7
  38. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/RECORD +41 -38
  39. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -366
  40. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/LICENSE +0 -0
  41. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/WHEEL +0 -0
  42. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/entry_points.txt +0 -0
@@ -30,8 +30,9 @@ class PolymarketSubgraphHandler(BaseSubgraphHandler):
30
30
  def get_conditions(
31
31
  self, condition_ids: list[HexBytes]
32
32
  ) -> list[ConditionSubgraphModel]:
33
- where_stms = {"id_in": [i.hex() for i in condition_ids]}
33
+ where_stms = {"id_in": [i.to_0x_hex() for i in condition_ids]}
34
34
  conditions = self.conditions_subgraph.Query.conditions(
35
+ first=len(condition_ids),
35
36
  where=where_stms,
36
37
  )
37
38
 
@@ -1,5 +1,6 @@
1
1
  import typing as t
2
2
  from datetime import timedelta
3
+ from enum import Enum
3
4
  from typing import Annotated
4
5
  from urllib.parse import urljoin
5
6
 
@@ -139,11 +140,6 @@ class SeerMarket(BaseModel):
139
140
  for token in self.wrapped_tokens
140
141
  ]
141
142
 
142
- @property
143
- def is_binary(self) -> bool:
144
- # 3 because Seer has also third, `Invalid` outcome.
145
- return len(self.outcomes) == 3
146
-
147
143
  @property
148
144
  def collateral_token_contract_address_checksummed(self) -> ChecksumAddress:
149
145
  return Web3.to_checksum_address(self.collateral_token)
@@ -160,7 +156,7 @@ class SeerMarket(BaseModel):
160
156
  @property
161
157
  def url(self) -> str:
162
158
  chain_id = RPCConfig().chain_id
163
- return urljoin(SEER_BASE_URL, f"markets/{chain_id}/{self.id.hex()}")
159
+ return urljoin(SEER_BASE_URL, f"markets/{chain_id}/{self.id.to_0x_hex()}")
164
160
 
165
161
 
166
162
  class SeerMarketWithQuestions(SeerMarket):
@@ -188,3 +184,42 @@ class ExactInputSingleParams(BaseModel):
188
184
  limit_sqrt_price: Wei = Field(
189
185
  alias="limitSqrtPrice", default_factory=lambda: Wei(0)
190
186
  ) # 0 for convenience, we also don't expect major price shifts
187
+
188
+
189
+ class SeerTransactionType(str, Enum):
190
+ SWAP = "swap"
191
+ SPLIT = "split"
192
+
193
+
194
+ class SeerTransaction(BaseModel):
195
+ model_config = ConfigDict(populate_by_name=True)
196
+
197
+ market_name: str = Field(alias="marketName")
198
+ market_id: HexBytes = Field(alias="marketId")
199
+ type: SeerTransactionType
200
+ block_number: int = Field(alias="blockNumber")
201
+ transaction_hash: HexBytes = Field(alias="transactionHash")
202
+ collateral: HexAddress
203
+ collateral_symbol: str = Field(alias="collateralSymbol")
204
+
205
+ token_in: HexAddress = Field(alias="tokenIn")
206
+ token_out: HexAddress = Field(alias="tokenOut")
207
+ amount_in: Wei = Field(alias="amountIn")
208
+ amount_out: Wei = Field(alias="amountOut")
209
+ token_in_symbol: str = Field(alias="tokenInSymbol")
210
+ token_out_symbol: str = Field(alias="tokenOutSymbol")
211
+ timestamp: int | None = None
212
+
213
+ amount: Wei | None = None
214
+
215
+ @property
216
+ def timestamp_dt(self) -> DatetimeUTC | None:
217
+ return DatetimeUTC.to_datetime_utc(self.timestamp) if self.timestamp else None
218
+
219
+ @property
220
+ def token_in_checksum(self) -> ChecksumAddress:
221
+ return Web3.to_checksum_address(self.token_in)
222
+
223
+ @property
224
+ def token_out_checksum(self) -> ChecksumAddress:
225
+ return Web3.to_checksum_address(self.token_out)
@@ -2,11 +2,15 @@ from cachetools import TTLCache, cached
2
2
  from pydantic import BaseModel
3
3
  from web3 import Web3
4
4
 
5
+ from prediction_market_agent_tooling.deploy.constants import (
6
+ INVALID_OUTCOME_LOWERCASE_IDENTIFIER,
7
+ )
5
8
  from prediction_market_agent_tooling.gtypes import (
6
9
  ChecksumAddress,
7
10
  CollateralToken,
8
11
  HexAddress,
9
12
  OutcomeStr,
13
+ OutcomeToken,
10
14
  Probability,
11
15
  )
12
16
  from prediction_market_agent_tooling.loggers import logger
@@ -46,7 +50,7 @@ class PriceManager:
46
50
  price_diff_pct = abs(old_price - normalized_price) / max(old_price, 0.01)
47
51
  if price_diff_pct > max_price_diff:
48
52
  logger.info(
49
- f"{price_diff_pct=} larger than {max_price_diff=} for seer market {self.seer_market.id.hex()} "
53
+ f"{price_diff_pct=} larger than {max_price_diff=} for seer market {self.seer_market.id.to_0x_hex()} "
50
54
  )
51
55
 
52
56
  def get_price_for_token(self, token: ChecksumAddress) -> CollateralToken | None:
@@ -182,3 +186,67 @@ class PriceManager:
182
186
  normalized_prices[outcome] = new_price
183
187
 
184
188
  return normalized_prices
189
+
190
+ def build_initial_probs_from_pool(
191
+ self, model: SeerMarket, wrapped_tokens: list[ChecksumAddress]
192
+ ) -> tuple[dict[OutcomeStr, Probability], dict[OutcomeStr, OutcomeToken]]:
193
+ """
194
+ Builds a map of outcome to probability and outcome token pool.
195
+ """
196
+ probability_map = {}
197
+ outcome_token_pool = {}
198
+ wrapped_tokens_with_supply = [
199
+ (
200
+ token,
201
+ SeerSubgraphHandler().get_pool_by_token(
202
+ token, model.collateral_token_contract_address_checksummed
203
+ ),
204
+ )
205
+ for token in wrapped_tokens
206
+ ]
207
+ wrapped_tokens_with_supply = [
208
+ (token, pool)
209
+ for token, pool in wrapped_tokens_with_supply
210
+ if pool is not None
211
+ ]
212
+
213
+ for token, pool in wrapped_tokens_with_supply:
214
+ if pool is None or pool.token1.id is None or pool.token0.id is None:
215
+ continue
216
+ if HexBytes(token) == HexBytes(pool.token1.id):
217
+ outcome_token_pool[
218
+ OutcomeStr(model.outcomes[wrapped_tokens.index(token)])
219
+ ] = (
220
+ OutcomeToken(pool.totalValueLockedToken0)
221
+ if pool.totalValueLockedToken0 is not None
222
+ else OutcomeToken(0)
223
+ )
224
+ probability_map[
225
+ OutcomeStr(model.outcomes[wrapped_tokens.index(token)])
226
+ ] = Probability(pool.token0Price.value)
227
+ else:
228
+ outcome_token_pool[
229
+ OutcomeStr(model.outcomes[wrapped_tokens.index(token)])
230
+ ] = (
231
+ OutcomeToken(pool.totalValueLockedToken1)
232
+ if pool.totalValueLockedToken1 is not None
233
+ else OutcomeToken(0)
234
+ )
235
+ probability_map[
236
+ OutcomeStr(model.outcomes[wrapped_tokens.index(token)])
237
+ ] = Probability(pool.token1Price.value)
238
+
239
+ for outcome in model.outcomes:
240
+ if outcome not in outcome_token_pool:
241
+ outcome_token_pool[outcome] = OutcomeToken(0)
242
+ logger.warning(
243
+ f"Outcome {outcome} not found in outcome_token_pool for market {self.seer_market.url}."
244
+ )
245
+ if outcome not in probability_map:
246
+ if INVALID_OUTCOME_LOWERCASE_IDENTIFIER not in outcome.lower():
247
+ raise PriceCalculationError(
248
+ f"Couldn't get probability for {outcome} for market {self.seer_market.url}."
249
+ )
250
+ else:
251
+ probability_map[outcome] = Probability(0)
252
+ return probability_map, outcome_token_pool
@@ -28,7 +28,6 @@ from prediction_market_agent_tooling.markets.agent_market import (
28
28
  FilterBy,
29
29
  ParentMarket,
30
30
  ProcessedMarket,
31
- ProcessedTradedMarket,
32
31
  QuestionType,
33
32
  SortBy,
34
33
  )
@@ -36,9 +35,13 @@ from prediction_market_agent_tooling.markets.blockchain_utils import store_trade
36
35
  from prediction_market_agent_tooling.markets.data_models import (
37
36
  ExistingPosition,
38
37
  Resolution,
38
+ ResolvedBet,
39
39
  )
40
40
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
41
- from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
41
+ from prediction_market_agent_tooling.markets.omen.omen import (
42
+ OmenAgentMarket,
43
+ send_keeping_token_to_eoa_xdai,
44
+ )
42
45
  from prediction_market_agent_tooling.markets.omen.omen_constants import (
43
46
  SDAI_CONTRACT_ADDRESS,
44
47
  )
@@ -54,6 +57,7 @@ from prediction_market_agent_tooling.markets.seer.exceptions import (
54
57
  PriceCalculationError,
55
58
  )
56
59
  from prediction_market_agent_tooling.markets.seer.price_manager import PriceManager
60
+ from prediction_market_agent_tooling.markets.seer.seer_api import get_seer_transactions
57
61
  from prediction_market_agent_tooling.markets.seer.seer_contracts import (
58
62
  GnosisRouter,
59
63
  SeerMarketFactory,
@@ -79,7 +83,6 @@ from prediction_market_agent_tooling.tools.cow.cow_order import (
79
83
  OrderStatusError,
80
84
  get_orders_by_owner,
81
85
  get_trades_by_order_uid,
82
- get_trades_by_owner,
83
86
  swap_tokens_waiting,
84
87
  wait_for_order_completion,
85
88
  )
@@ -136,7 +139,7 @@ class SeerAgentMarket(AgentMarket):
136
139
 
137
140
  def store_trades(
138
141
  self,
139
- traded_market: ProcessedTradedMarket | None,
142
+ traded_market: ProcessedMarket | None,
140
143
  keys: APIKeys,
141
144
  agent_name: str,
142
145
  web3: Web3 | None = None,
@@ -262,10 +265,6 @@ class SeerAgentMarket(AgentMarket):
262
265
  amounts_ot=amounts_ot,
263
266
  )
264
267
 
265
- @staticmethod
266
- def get_user_id(api_keys: APIKeys) -> str:
267
- return OmenAgentMarket.get_user_id(api_keys)
268
-
269
268
  @staticmethod
270
269
  def _filter_markets_contained_in_trades(
271
270
  api_keys: APIKeys,
@@ -274,10 +273,12 @@ class SeerAgentMarket(AgentMarket):
274
273
  """
275
274
  We filter the markets using previous trades by the user so that we don't have to process all Seer markets.
276
275
  """
277
- trades_by_user = get_trades_by_owner(api_keys.bet_from_address)
276
+ trades_by_user = get_seer_transactions(
277
+ api_keys.bet_from_address, RPCConfig().CHAIN_ID
278
+ )
278
279
 
279
- traded_tokens = {t.buyToken for t in trades_by_user}.union(
280
- [t.sellToken for t in trades_by_user]
280
+ traded_tokens = {t.token_in_checksum for t in trades_by_user}.union(
281
+ [t.token_out_checksum for t in trades_by_user]
281
282
  )
282
283
  filtered_markets: list[SeerMarket] = []
283
284
  for market in markets:
@@ -315,19 +316,43 @@ class SeerAgentMarket(AgentMarket):
315
316
  for market in filtered_markets
316
317
  if market.is_redeemable(owner=api_keys.bet_from_address, web3=web3)
317
318
  ]
319
+ logger.info(f"Got {len(markets_to_redeem)} markets to redeem on Seer.")
318
320
 
319
321
  gnosis_router = GnosisRouter()
320
322
  for market in markets_to_redeem:
321
323
  try:
324
+ # GnosisRouter needs approval to use our outcome tokens
325
+ for i, token in enumerate(market.wrapped_tokens):
326
+ ContractERC20OnGnosisChain(
327
+ address=Web3.to_checksum_address(token)
328
+ ).approve(
329
+ api_keys,
330
+ for_address=gnosis_router.address,
331
+ amount_wei=market_balances[market.id][i].as_wei,
332
+ web3=web3,
333
+ )
334
+
335
+ # We can only ask for redeem of outcome tokens on correct outcomes
336
+ # TODO: Implement more complex use-cases: https://github.com/gnosis/prediction-market-agent-tooling/issues/850
337
+ amounts_to_redeem = [
338
+ (amount if numerator > 0 else OutcomeWei(0))
339
+ for amount, numerator in zip(
340
+ market_balances[market.id], market.payout_numerators
341
+ )
342
+ ]
343
+
344
+ # Redeem!
322
345
  params = RedeemParams(
323
346
  market=Web3.to_checksum_address(market.id),
324
347
  outcome_indices=list(range(len(market.payout_numerators))),
325
- amounts=market_balances[market.id],
348
+ amounts=amounts_to_redeem,
326
349
  )
327
350
  gnosis_router.redeem_to_base(api_keys, params=params, web3=web3)
328
- logger.info(f"Redeemed market {market.id.hex()}")
329
- except Exception as e:
330
- logger.error(f"Failed to redeem market {market.id.hex()}, {e}")
351
+ logger.info(f"Redeemed market {market.url}.")
352
+ except Exception:
353
+ logger.exception(
354
+ f"Failed to redeem market {market.url}, {market.outcomes}, with amounts {market_balances[market.id]} and payout numerators {market.payout_numerators}, and wrapped tokens {market.wrapped_tokens}."
355
+ )
331
356
 
332
357
  # GnosisRouter withdraws sDai into wxDAI/xDai on its own, so no auto-withdraw needed by us.
333
358
 
@@ -345,6 +370,17 @@ class SeerAgentMarket(AgentMarket):
345
370
 
346
371
  return False
347
372
 
373
+ def ensure_min_native_balance(
374
+ self,
375
+ min_required_balance: xDai,
376
+ multiplier: float = 3.0,
377
+ ) -> None:
378
+ send_keeping_token_to_eoa_xdai(
379
+ api_keys=APIKeys(),
380
+ min_required_balance=min_required_balance,
381
+ multiplier=multiplier,
382
+ )
383
+
348
384
  @staticmethod
349
385
  def verify_operational_balance(api_keys: APIKeys) -> bool:
350
386
  return OmenAgentMarket.verify_operational_balance(api_keys=api_keys)
@@ -367,7 +403,7 @@ class SeerAgentMarket(AgentMarket):
367
403
  raise ValueError("Seer categorical markets must have 1 question.")
368
404
 
369
405
  question = model.questions[0]
370
- outcome = model.outcomes[int(question.question.best_answer.hex(), 16)]
406
+ outcome = model.outcomes[int(question.question.best_answer.to_0x_hex(), 16)]
371
407
  return Resolution(outcome=outcome, invalid=False)
372
408
 
373
409
  @staticmethod
@@ -411,13 +447,17 @@ class SeerAgentMarket(AgentMarket):
411
447
  must_have_prices: bool,
412
448
  ) -> t.Optional["SeerAgentMarket"]:
413
449
  price_manager = PriceManager(seer_market=model, seer_subgraph=seer_subgraph)
414
-
415
- probability_map = {}
450
+ wrapped_tokens = [Web3.to_checksum_address(i) for i in model.wrapped_tokens]
416
451
  try:
417
- probability_map = price_manager.build_probability_map()
452
+ (
453
+ probability_map,
454
+ outcome_token_pool,
455
+ ) = price_manager.build_initial_probs_from_pool(
456
+ model=model, wrapped_tokens=wrapped_tokens
457
+ )
418
458
  except PriceCalculationError as e:
419
459
  logger.info(
420
- f"Error when calculating probabilities for market {model.id.hex()} - {e}"
460
+ f"Error when calculating probabilities for market {model.id.to_0x_hex()} - {e}"
421
461
  )
422
462
  if must_have_prices:
423
463
  # Price calculation failed, so don't return the market
@@ -428,7 +468,7 @@ class SeerAgentMarket(AgentMarket):
428
468
  parent = SeerAgentMarket.get_parent(model=model, seer_subgraph=seer_subgraph)
429
469
 
430
470
  market = SeerAgentMarket(
431
- id=model.id.hex(),
471
+ id=model.id.to_0x_hex(),
432
472
  question=model.title,
433
473
  creator=model.creator,
434
474
  created_time=model.created_time,
@@ -437,9 +477,9 @@ class SeerAgentMarket(AgentMarket):
437
477
  condition_id=model.condition_id,
438
478
  url=model.url,
439
479
  close_time=model.close_time,
440
- wrapped_tokens=[Web3.to_checksum_address(i) for i in model.wrapped_tokens],
480
+ wrapped_tokens=wrapped_tokens,
441
481
  fees=MarketFees.get_zero_fees(),
442
- outcome_token_pool=None,
482
+ outcome_token_pool=outcome_token_pool,
443
483
  outcomes_supply=model.outcomes_supply,
444
484
  resolution=resolution,
445
485
  volume=None,
@@ -451,6 +491,15 @@ class SeerAgentMarket(AgentMarket):
451
491
 
452
492
  return market
453
493
 
494
+ @staticmethod
495
+ def get_resolved_bets_made_since(
496
+ better_address: ChecksumAddress,
497
+ start_time: DatetimeUTC,
498
+ end_time: DatetimeUTC | None,
499
+ ) -> list[ResolvedBet]:
500
+ # TODO: https://github.com/gnosis/prediction-market-agent-tooling/issues/841
501
+ raise NotImplementedError()
502
+
454
503
  @staticmethod
455
504
  def get_markets(
456
505
  limit: int,
@@ -521,7 +570,9 @@ class SeerAgentMarket(AgentMarket):
521
570
  )
522
571
 
523
572
  token_balance = token_contract.balance_of_in_tokens(
524
- for_address=Web3.to_checksum_address(HexAddress(HexStr(pool.id.hex()))),
573
+ for_address=Web3.to_checksum_address(
574
+ HexAddress(HexStr(pool.id.to_0x_hex()))
575
+ ),
525
576
  web3=web3,
526
577
  )
527
578
  collateral_balance = p.get_amount_of_token_in_collateral(
@@ -602,7 +653,7 @@ class SeerAgentMarket(AgentMarket):
602
653
  )
603
654
  cow_tx_hash = trades[0].txHash
604
655
  logger.info(f"TxHash is {cow_tx_hash=} for {order_metadata.uid.root=}.")
605
- return cow_tx_hash.hex()
656
+ return cow_tx_hash.to_0x_hex()
606
657
 
607
658
  except (
608
659
  UnexpectedResponseError,
@@ -635,7 +686,7 @@ class SeerAgentMarket(AgentMarket):
635
686
  )
636
687
  swap_pool_tx_hash = tx_receipt["transactionHash"]
637
688
  logger.info(f"TxHash is {swap_pool_tx_hash=}.")
638
- return swap_pool_tx_hash.hex()
689
+ return swap_pool_tx_hash.to_0x_hex()
639
690
 
640
691
  def place_bet(
641
692
  self,
@@ -0,0 +1,28 @@
1
+ import httpx
2
+
3
+ from prediction_market_agent_tooling.gtypes import ChainID, ChecksumAddress
4
+ from prediction_market_agent_tooling.markets.seer.data_models import SeerTransaction
5
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
6
+ from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
7
+
8
+
9
+ def get_seer_transactions(
10
+ account: ChecksumAddress,
11
+ chain_id: ChainID,
12
+ start_time: DatetimeUTC | None = None,
13
+ end_time: DatetimeUTC | None = None,
14
+ timeout: int = 60, # The endpoint is pretty slow to respond atm.
15
+ ) -> list[SeerTransaction]:
16
+ url = "https://app.seer.pm/.netlify/functions/get-transactions"
17
+ params: dict[str, str | int] = {
18
+ "account": account,
19
+ "chainId": chain_id,
20
+ "startTime": to_int_timestamp(start_time) if start_time else 0,
21
+ "endTime": to_int_timestamp(end_time if end_time else utcnow()),
22
+ }
23
+ response = httpx.get(url, params=params, timeout=timeout)
24
+ response.raise_for_status()
25
+ response_json = response.json()
26
+
27
+ transactions = [SeerTransaction.model_validate(tx) for tx in response_json]
28
+ return transactions
@@ -29,10 +29,17 @@ from prediction_market_agent_tooling.markets.seer.data_models import (
29
29
  SeerMarketQuestions,
30
30
  SeerMarketWithQuestions,
31
31
  )
32
- from prediction_market_agent_tooling.markets.seer.subgraph_data_models import SeerPool
32
+ from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
33
+ SeerPool,
34
+ SwaprSwap,
35
+ )
33
36
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
34
37
  from prediction_market_agent_tooling.tools.singleton import SingletonMeta
35
- from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
38
+ from prediction_market_agent_tooling.tools.utils import (
39
+ DatetimeUTC,
40
+ to_int_timestamp,
41
+ utcnow,
42
+ )
36
43
  from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
37
44
 
38
45
 
@@ -155,7 +162,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
155
162
  raise ValueError(f"Unknown filter {filter_by}")
156
163
 
157
164
  if parent_market_id:
158
- and_stms["parentMarket"] = parent_market_id.hex().lower()
165
+ and_stms["parentMarket"] = parent_market_id.to_0x_hex().lower()
159
166
 
160
167
  outcome_filters: list[dict[str, t.Any]] = []
161
168
 
@@ -294,7 +301,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
294
301
  self, market_ids: list[HexBytes]
295
302
  ) -> list[SeerMarketQuestions]:
296
303
  where = unwrap_generic_value(
297
- {"market_in": [market_id.hex().lower() for market_id in market_ids]}
304
+ {"market_in": [market_id.to_0x_hex().lower() for market_id in market_ids]}
298
305
  )
299
306
  markets_field = self.seer_subgraph.Query.marketQuestions(where=where)
300
307
  fields = self._get_fields_for_questions(markets_field)
@@ -302,7 +309,9 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
302
309
  return questions
303
310
 
304
311
  def get_market_by_id(self, market_id: HexBytes) -> SeerMarketWithQuestions:
305
- markets_field = self.seer_subgraph.Query.market(id=market_id.hex().lower())
312
+ markets_field = self.seer_subgraph.Query.market(
313
+ id=market_id.to_0x_hex().lower()
314
+ )
306
315
  fields = self._get_fields_for_markets(markets_field)
307
316
  markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
308
317
  if len(markets) != 1:
@@ -326,8 +335,8 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
326
335
  ]
327
336
  return fields
328
337
 
329
- def get_market_by_wrapped_token(self, token: ChecksumAddress) -> SeerMarket:
330
- where_stms = {"wrappedTokens_contains": [token]}
338
+ def get_market_by_wrapped_token(self, tokens: list[ChecksumAddress]) -> SeerMarket:
339
+ where_stms = {"wrappedTokens_contains": tokens}
331
340
  markets_field = self.seer_subgraph.Query.markets(
332
341
  where=unwrap_generic_value(where_stms)
333
342
  )
@@ -339,20 +348,27 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
339
348
  )
340
349
  return markets[0]
341
350
 
342
- def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
343
- fields = [
344
- pools_field.id,
345
- pools_field.liquidity,
346
- pools_field.sqrtPrice,
347
- pools_field.token0Price,
348
- pools_field.token1Price,
349
- pools_field.token0.id,
350
- pools_field.token0.name,
351
- pools_field.token0.symbol,
352
- pools_field.token1.id,
353
- pools_field.token1.name,
354
- pools_field.token1.symbol,
351
+ def _get_fields_for_seer_token(self, fields: FieldPath) -> list[FieldPath]:
352
+ return [
353
+ fields.id,
354
+ fields.name,
355
+ fields.symbol,
355
356
  ]
357
+
358
+ def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
359
+ fields = (
360
+ [
361
+ pools_field.id,
362
+ pools_field.liquidity,
363
+ pools_field.sqrtPrice,
364
+ pools_field.token0Price,
365
+ pools_field.token1Price,
366
+ pools_field.totalValueLockedToken0,
367
+ pools_field.totalValueLockedToken1,
368
+ ]
369
+ + self._get_fields_for_seer_token(pools_field.token0)
370
+ + self._get_fields_for_seer_token(pools_field.token1)
371
+ )
356
372
  return fields
357
373
 
358
374
  def get_pool_by_token(
@@ -392,6 +408,41 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
392
408
  return pools[0]
393
409
  return None
394
410
 
411
+ def _get_fields_for_swaps(self, swaps_field: FieldPath) -> list[FieldPath]:
412
+ fields = (
413
+ [
414
+ swaps_field.id,
415
+ swaps_field.pool.id,
416
+ swaps_field.sender,
417
+ swaps_field.recipient,
418
+ swaps_field.price,
419
+ swaps_field.amount0,
420
+ swaps_field.amount1,
421
+ swaps_field.timestamp,
422
+ ]
423
+ + self._get_fields_for_seer_token(swaps_field.token0)
424
+ + self._get_fields_for_seer_token(swaps_field.token1)
425
+ )
426
+ return fields
427
+
428
+ def get_swaps(
429
+ self,
430
+ recipient: ChecksumAddress,
431
+ timestamp_gt: DatetimeUTC | None = None,
432
+ timestamp_lt: DatetimeUTC | None = None,
433
+ ) -> list[SwaprSwap]:
434
+ where_argument: dict[str, Any] = {"recipient": recipient.lower()}
435
+ if timestamp_gt is not None:
436
+ where_argument["timestamp_gt"] = to_int_timestamp(timestamp_gt)
437
+ if timestamp_lt is not None:
438
+ where_argument["timestamp_lt"] = to_int_timestamp(timestamp_lt)
439
+
440
+ swaps_field = self.swapr_algebra_subgraph.Query.swaps(where=where_argument)
441
+ fields = self._get_fields_for_swaps(swaps_field)
442
+ swaps = self.do_query(fields=fields, pydantic_model=SwaprSwap)
443
+
444
+ return swaps
445
+
395
446
 
396
447
  class SeerQuestionsCache(metaclass=SingletonMeta):
397
448
  """A singleton cache for storing and retrieving Seer market questions.
@@ -1,12 +1,17 @@
1
1
  from pydantic import BaseModel, ConfigDict, Field
2
+ from web3 import Web3
2
3
  from web3.constants import ADDRESS_ZERO
3
4
 
4
5
  from prediction_market_agent_tooling.gtypes import (
6
+ ChecksumAddress,
5
7
  CollateralToken,
6
8
  HexAddress,
7
9
  HexBytes,
8
10
  OutcomeStr,
11
+ OutcomeToken,
12
+ Wei,
9
13
  )
14
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
10
15
 
11
16
 
12
17
  class SeerToken(BaseModel):
@@ -14,6 +19,10 @@ class SeerToken(BaseModel):
14
19
  name: str
15
20
  symbol: str
16
21
 
22
+ @property
23
+ def address(self) -> ChecksumAddress:
24
+ return Web3.to_checksum_address(self.id.hex())
25
+
17
26
 
18
27
  class SeerPool(BaseModel):
19
28
  model_config = ConfigDict(populate_by_name=True)
@@ -24,6 +33,64 @@ class SeerPool(BaseModel):
24
33
  token0Price: CollateralToken
25
34
  token1Price: CollateralToken
26
35
  sqrtPrice: int
36
+ totalValueLockedToken0: float
37
+ totalValueLockedToken1: float
38
+
39
+
40
+ class SwaprSwap(BaseModel):
41
+ id: str # It's like "0x73afd8f096096552d72a0b40ea66d2076be136c6a531e2f6b190d151a750271e#32" (note the #32) # web3-private-key-ok
42
+ recipient: HexAddress
43
+ sender: HexAddress
44
+ price: Wei
45
+ amount0: CollateralToken
46
+ amount1: CollateralToken
47
+ token0: SeerToken
48
+ token1: SeerToken
49
+ timestamp: int
50
+
51
+ @property
52
+ def timestamp_utc(self) -> DatetimeUTC:
53
+ return DatetimeUTC.to_datetime_utc(self.timestamp)
54
+
55
+ @property
56
+ def added_to_pool(self) -> CollateralToken:
57
+ return self.amount0 if self.amount0 > 0 else self.amount1
58
+
59
+ @property
60
+ def withdrawn_from_pool(self) -> OutcomeToken:
61
+ return (
62
+ OutcomeToken(abs(self.amount0).value)
63
+ if self.amount0 < 0
64
+ else OutcomeToken(abs(self.amount1).value)
65
+ )
66
+
67
+
68
+ class SeerSwap(BaseModel):
69
+ id: str # It's like "0x73afd8f096096552d72a0b40ea66d2076be136c6a531e2f6b190d151a750271e#32" (note the #32) # web3-private-key-ok
70
+ recipient: HexAddress
71
+ sender: HexAddress
72
+ price: Wei
73
+ amount0: CollateralToken
74
+ amount1: CollateralToken
75
+ token0: SeerToken
76
+ token1: SeerToken
77
+ timestamp: int
78
+
79
+ @property
80
+ def timestamp_utc(self) -> DatetimeUTC:
81
+ return DatetimeUTC.to_datetime_utc(self.timestamp)
82
+
83
+ @property
84
+ def buying_collateral_amount(self) -> CollateralToken:
85
+ return self.amount0 if self.amount0 > 0 else self.amount1
86
+
87
+ @property
88
+ def received_shares_amount(self) -> OutcomeToken:
89
+ return (
90
+ OutcomeToken(abs(self.amount0).value)
91
+ if self.amount0 < 0
92
+ else OutcomeToken(abs(self.amount1).value)
93
+ )
27
94
 
28
95
 
29
96
  class NewMarketEvent(BaseModel):