prediction-market-agent-tooling 0.66.5__py3-none-any.whl → 0.67.1__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 (28) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +23 -5
  2. prediction_market_agent_tooling/markets/agent_market.py +9 -2
  3. prediction_market_agent_tooling/markets/manifold/manifold.py +3 -2
  4. prediction_market_agent_tooling/markets/markets.py +5 -5
  5. prediction_market_agent_tooling/markets/metaculus/metaculus.py +3 -1
  6. prediction_market_agent_tooling/markets/omen/omen.py +5 -2
  7. prediction_market_agent_tooling/markets/polymarket/api.py +87 -104
  8. prediction_market_agent_tooling/markets/polymarket/data_models.py +60 -14
  9. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -54
  10. prediction_market_agent_tooling/markets/polymarket/polymarket.py +109 -26
  11. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +49 -0
  12. prediction_market_agent_tooling/markets/polymarket/utils.py +0 -21
  13. prediction_market_agent_tooling/markets/seer/price_manager.py +65 -46
  14. prediction_market_agent_tooling/markets/seer/seer.py +70 -90
  15. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +48 -35
  16. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +11 -1
  17. prediction_market_agent_tooling/tools/cow/cow_order.py +26 -11
  18. prediction_market_agent_tooling/tools/cow/models.py +4 -2
  19. prediction_market_agent_tooling/tools/httpx_cached_client.py +13 -6
  20. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +7 -0
  21. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  22. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  23. prediction_market_agent_tooling/tools/utils.py +5 -2
  24. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/METADATA +1 -1
  25. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/RECORD +28 -26
  26. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/LICENSE +0 -0
  27. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/WHEEL +0 -0
  28. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/entry_points.txt +0 -0
@@ -24,6 +24,8 @@ from prediction_market_agent_tooling.loggers import logger
24
24
  from prediction_market_agent_tooling.markets.agent_market import (
25
25
  AgentMarket,
26
26
  FilterBy,
27
+ MarketFees,
28
+ MarketType,
27
29
  ProcessedMarket,
28
30
  ProcessedTradedMarket,
29
31
  SortBy,
@@ -66,8 +68,9 @@ from prediction_market_agent_tooling.tools.contract import (
66
68
  )
67
69
  from prediction_market_agent_tooling.tools.cow.cow_order import (
68
70
  NoLiquidityAvailableOnCowException,
69
- get_buy_token_amount_else_raise,
71
+ OrderStatusError,
70
72
  get_orders_by_owner,
73
+ get_trades_by_order_uid,
71
74
  get_trades_by_owner,
72
75
  swap_tokens_waiting,
73
76
  wait_for_order_completion,
@@ -76,6 +79,9 @@ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
76
79
  from prediction_market_agent_tooling.tools.tokens.auto_deposit import (
77
80
  auto_deposit_collateral_token,
78
81
  )
82
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
83
+ get_slippage_tolerance_per_token,
84
+ )
79
85
  from prediction_market_agent_tooling.tools.tokens.usd import (
80
86
  get_token_in_usd,
81
87
  get_usd_in_token,
@@ -95,6 +101,7 @@ class SeerAgentMarket(AgentMarket):
95
101
  None # Seer markets don't have a description, so just default to None.
96
102
  )
97
103
  outcomes_supply: int
104
+ minimum_market_liquidity_required: CollateralToken = CollateralToken(1)
98
105
 
99
106
  def get_collateral_token_contract(
100
107
  self, web3: Web3 | None = None
@@ -131,45 +138,34 @@ class SeerAgentMarket(AgentMarket):
131
138
  web3=web3,
132
139
  )
133
140
 
141
+ def get_price_manager(self) -> PriceManager:
142
+ return PriceManager.build(HexBytes(HexStr(self.id)))
143
+
134
144
  def get_token_in_usd(self, x: CollateralToken) -> USD:
135
- try:
136
- return get_token_in_usd(
137
- x, self.collateral_token_contract_address_checksummed
138
- )
139
- except NoLiquidityAvailableOnCowException as e:
140
- logger.warning(
141
- f"Could not get quote for {self.collateral_token_contract_address_checksummed} from Cow, exception {e=}. Falling back to pools. "
145
+ p = self.get_price_manager()
146
+ sdai_amount = p.get_amount_of_collateral_in_token(
147
+ # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
148
+ SDAI_CONTRACT_ADDRESS,
149
+ x,
150
+ )
151
+ if sdai_amount is None:
152
+ raise RuntimeError(
153
+ "Both CoW and pool-fallback way of getting price failed."
142
154
  )
143
- usd_token_price = self.get_collateral_price_from_pools()
144
- if usd_token_price is None:
145
- raise RuntimeError(
146
- "Both CoW and pool-fallback way of getting price failed."
147
- ) from e
148
- return USD(x.value * usd_token_price.value)
149
-
150
- def get_collateral_price_from_pools(self) -> USD | None:
151
- p = PriceManager.build(HexBytes(HexStr(self.id)))
152
- token_price = p.get_token_price_from_pools(token=SDAI_CONTRACT_ADDRESS)
153
- if token_price:
154
- return get_token_in_usd(token_price, SDAI_CONTRACT_ADDRESS)
155
-
156
- return None
155
+ return get_token_in_usd(sdai_amount, SDAI_CONTRACT_ADDRESS)
157
156
 
158
157
  def get_usd_in_token(self, x: USD) -> CollateralToken:
159
- try:
160
- return get_usd_in_token(
161
- x, self.collateral_token_contract_address_checksummed
162
- )
163
- except NoLiquidityAvailableOnCowException as e:
164
- logger.warning(
165
- f"Could not get quote for {self.collateral_token_contract_address_checksummed} from Cow, exception {e=}. Falling back to pools. "
158
+ p = self.get_price_manager()
159
+ token_amount = p.get_amount_of_token_in_collateral(
160
+ # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
161
+ SDAI_CONTRACT_ADDRESS,
162
+ get_usd_in_token(x, SDAI_CONTRACT_ADDRESS),
163
+ )
164
+ if token_amount is None:
165
+ raise RuntimeError(
166
+ "Both CoW and pool-fallback way of getting price failed."
166
167
  )
167
- usd_token_price = self.get_collateral_price_from_pools()
168
- if not usd_token_price:
169
- raise RuntimeError(
170
- "Both CoW and pool-fallback way of getting price failed."
171
- ) from e
172
- return CollateralToken(x.value / usd_token_price.value)
168
+ return token_amount
173
169
 
174
170
  def get_buy_token_amount(
175
171
  self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
@@ -185,16 +181,15 @@ class SeerAgentMarket(AgentMarket):
185
181
 
186
182
  bet_amount_in_tokens = self.get_in_token(bet_amount)
187
183
 
188
- p = PriceManager.build(market_id=HexBytes(HexStr(self.id)))
189
- price = p.get_price_for_token(
184
+ p = self.get_price_manager()
185
+ amount_outcome_tokens = p.get_amount_of_collateral_in_token(
190
186
  token=outcome_token, collateral_exchange_amount=bet_amount_in_tokens
191
187
  )
192
- if not price:
188
+ if not amount_outcome_tokens:
193
189
  logger.info(f"Could not get price for token {outcome_token}")
194
190
  return None
195
191
 
196
- amount_outcome_tokens = bet_amount_in_tokens / price
197
- return OutcomeToken(amount_outcome_tokens)
192
+ return OutcomeToken(amount_outcome_tokens.value)
198
193
 
199
194
  def get_sell_value_of_outcome_token(
200
195
  self, outcome: OutcomeStr, amount: OutcomeToken
@@ -203,26 +198,18 @@ class SeerAgentMarket(AgentMarket):
203
198
  return CollateralToken.zero()
204
199
 
205
200
  wrapped_outcome_token = self.get_wrapped_token_for_outcome(outcome)
206
- try:
207
- # We calculate how much collateral we would get back if we sold `amount` of outcome token.
208
- value_outcome_token_in_collateral = get_buy_token_amount_else_raise(
209
- sell_amount=amount.as_outcome_wei.as_wei,
210
- sell_token=wrapped_outcome_token,
211
- buy_token=self.collateral_token_contract_address_checksummed,
212
- )
213
- return value_outcome_token_in_collateral.as_token
214
- except NoLiquidityAvailableOnCowException as e:
215
- logger.warning(
216
- f"No liquidity available on Cow for {wrapped_outcome_token} -> {self.collateral_token_contract_address_checksummed}."
201
+
202
+ p = self.get_price_manager()
203
+ value_outcome_token_in_collateral = p.get_amount_of_token_in_collateral(
204
+ wrapped_outcome_token, amount.as_token
205
+ )
206
+
207
+ if value_outcome_token_in_collateral is None:
208
+ raise RuntimeError(
209
+ f"Could not get price for token from pools for {wrapped_outcome_token}"
217
210
  )
218
- p = PriceManager.build(market_id=HexBytes(HexStr(self.id)))
219
- price = p.get_token_price_from_pools(token=wrapped_outcome_token)
220
- if not price:
221
- logger.info(
222
- f"Could not get price for token from pools for {wrapped_outcome_token}"
223
- )
224
- raise e
225
- return CollateralToken(price.value * amount.value)
211
+
212
+ return value_outcome_token_in_collateral
226
213
 
227
214
  @staticmethod
228
215
  def get_trade_balance(api_keys: APIKeys) -> USD:
@@ -231,9 +218,7 @@ class SeerAgentMarket(AgentMarket):
231
218
  def get_tiny_bet_amount(self) -> CollateralToken:
232
219
  return self.get_in_token(SEER_TINY_BET_AMOUNT)
233
220
 
234
- def get_position_else_raise(
235
- self, user_id: str, web3: Web3 | None = None
236
- ) -> ExistingPosition:
221
+ def get_position(self, user_id: str, web3: Web3 | None = None) -> ExistingPosition:
237
222
  """
238
223
  Fetches position from the user in a given market.
239
224
  We ignore the INVALID balances since we are only interested in binary outcomes.
@@ -264,15 +249,6 @@ class SeerAgentMarket(AgentMarket):
264
249
  amounts_ot=amounts_ot,
265
250
  )
266
251
 
267
- def get_position(
268
- self, user_id: str, web3: Web3 | None = None
269
- ) -> ExistingPosition | None:
270
- try:
271
- return self.get_position_else_raise(user_id=user_id, web3=web3)
272
- except Exception as e:
273
- logger.warning(f"Could not get position for user {user_id}, exception {e}")
274
- return None
275
-
276
252
  @staticmethod
277
253
  def get_user_id(api_keys: APIKeys) -> str:
278
254
  return OmenAgentMarket.get_user_id(api_keys)
@@ -409,16 +385,16 @@ class SeerAgentMarket(AgentMarket):
409
385
  filter_by: FilterBy = FilterBy.OPEN,
410
386
  created_after: t.Optional[DatetimeUTC] = None,
411
387
  excluded_questions: set[str] | None = None,
412
- fetch_categorical_markets: bool = False,
413
- fetch_scalar_markets: bool = False,
388
+ market_type: MarketType = MarketType.ALL,
389
+ include_conditional_markets: bool = False,
414
390
  ) -> t.Sequence["SeerAgentMarket"]:
415
391
  seer_subgraph = SeerSubgraphHandler()
392
+
416
393
  markets = seer_subgraph.get_markets(
417
394
  limit=limit,
418
395
  sort_by=sort_by,
419
396
  filter_by=filter_by,
420
- include_categorical_markets=fetch_categorical_markets,
421
- include_only_scalar_markets=fetch_scalar_markets,
397
+ market_type=market_type,
422
398
  include_conditional_markets=False,
423
399
  )
424
400
 
@@ -461,7 +437,8 @@ class SeerAgentMarket(AgentMarket):
461
437
  f"Could not fetch pool for token {outcome_token}, no liquidity available for outcome."
462
438
  )
463
439
  return CollateralToken(0)
464
- p = PriceManager.build(HexBytes(HexStr(self.id)))
440
+
441
+ p = self.get_price_manager()
465
442
  total = CollateralToken(0)
466
443
 
467
444
  for token_address in [pool.token0.id, pool.token1.id]:
@@ -474,19 +451,13 @@ class SeerAgentMarket(AgentMarket):
474
451
  for_address=Web3.to_checksum_address(HexAddress(HexStr(pool.id.hex()))),
475
452
  web3=web3,
476
453
  )
477
-
478
- # get price
479
- token_price_in_sdai = (
480
- p.get_token_price_from_pools(token=token_address_checksummed)
481
- if token_address_checksummed
482
- != self.collateral_token_contract_address_checksummed
483
- else CollateralToken(1.0)
454
+ collateral_balance = p.get_amount_of_token_in_collateral(
455
+ token_address_checksummed, token_balance
484
456
  )
485
457
 
486
458
  # We ignore the liquidity in outcome tokens if price unknown.
487
- if token_price_in_sdai:
488
- sdai_balance = token_balance * token_price_in_sdai
489
- total += sdai_balance
459
+ if collateral_balance:
460
+ total += collateral_balance
490
461
 
491
462
  return total
492
463
 
@@ -501,7 +472,7 @@ class SeerAgentMarket(AgentMarket):
501
472
 
502
473
  def has_liquidity_for_outcome(self, outcome: OutcomeStr) -> bool:
503
474
  liquidity = self.get_liquidity_for_outcome(outcome)
504
- return liquidity > CollateralToken(0)
475
+ return liquidity > self.minimum_market_liquidity_required
505
476
 
506
477
  def has_liquidity(self) -> bool:
507
478
  # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
@@ -534,7 +505,7 @@ class SeerAgentMarket(AgentMarket):
534
505
  Returns:
535
506
  Transaction hash of the successful swap
536
507
  """
537
-
508
+ slippage_tolerance = get_slippage_tolerance_per_token(sell_token, buy_token)
538
509
  try:
539
510
  _, order = swap_tokens_waiting(
540
511
  amount_wei=amount_wei,
@@ -544,17 +515,26 @@ class SeerAgentMarket(AgentMarket):
544
515
  web3=web3,
545
516
  wait_order_complete=False,
546
517
  timeout=timedelta(minutes=2),
518
+ slippage_tolerance=slippage_tolerance,
547
519
  )
548
520
  order_metadata = asyncio.run(wait_for_order_completion(order=order))
549
- logger.debug(
521
+ logger.info(
550
522
  f"Swapped {sell_token} for {buy_token}. Order details {order_metadata}"
551
523
  )
552
- return order_metadata.uid.root
524
+ trades = get_trades_by_order_uid(HexBytes(order_metadata.uid.root))
525
+ if len(trades) != 1:
526
+ raise ValueError(
527
+ f"Expected exactly 1 trade from {order_metadata=}, but got {len(trades)=}."
528
+ )
529
+ cow_tx_hash = trades[0].txHash
530
+ logger.info(f"TxHash for {order_metadata.uid.root=} is {cow_tx_hash=}.")
531
+ return cow_tx_hash.hex()
553
532
 
554
533
  except (
555
534
  UnexpectedResponseError,
556
535
  TimeoutError,
557
536
  NoLiquidityAvailableOnCowException,
537
+ OrderStatusError,
558
538
  ) as e:
559
539
  # We don't retry if not enough balance.
560
540
  if "InsufficientBalance" in str(e):
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  import typing as t
3
+ from enum import Enum
3
4
  from typing import Any
4
5
 
5
6
  from subgrounds import FieldPath
@@ -13,7 +14,11 @@ from prediction_market_agent_tooling.deploy.constants import (
13
14
  )
14
15
  from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
15
16
  from prediction_market_agent_tooling.loggers import logger
16
- from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
17
+ from prediction_market_agent_tooling.markets.agent_market import (
18
+ FilterBy,
19
+ MarketType,
20
+ SortBy,
21
+ )
17
22
  from prediction_market_agent_tooling.markets.base_subgraph_handler import (
18
23
  BaseSubgraphHandler,
19
24
  )
@@ -24,6 +29,14 @@ from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
24
29
  from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
25
30
 
26
31
 
32
+ class TemplateId(int, Enum):
33
+ """Template IDs used in Reality.eth questions."""
34
+
35
+ SCALAR = 1
36
+ CATEGORICAL = 2
37
+ MULTICATEGORICAL = 3
38
+
39
+
27
40
  class SeerSubgraphHandler(BaseSubgraphHandler):
28
41
  """
29
42
  Class responsible for handling interactions with Seer subgraphs.
@@ -70,6 +83,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
70
83
  markets_field.collateralToken,
71
84
  markets_field.upperBound,
72
85
  markets_field.lowerBound,
86
+ markets_field.templateId,
73
87
  ]
74
88
  return fields
75
89
 
@@ -100,8 +114,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
100
114
  filter_by: FilterBy,
101
115
  outcome_supply_gt_if_open: Wei,
102
116
  include_conditional_markets: bool = False,
103
- include_categorical_markets: bool = True,
104
- include_only_scalar_markets: bool = False,
117
+ market_type: MarketType = MarketType.ALL,
105
118
  ) -> dict[Any, Any]:
106
119
  now = to_int_timestamp(utcnow())
107
120
 
@@ -123,43 +136,47 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
123
136
  if not include_conditional_markets:
124
137
  and_stms["parentMarket"] = ADDRESS_ZERO.lower()
125
138
 
126
- yes_stms, no_stms = {}, {}
127
- exclude_scalar_yes, exclude_scalar_no = {}, {}
139
+ outcome_filters: list[dict[str, t.Any]] = []
128
140
 
129
- # Return scalar markets.
130
- if include_only_scalar_markets:
131
- # We are interested in scalar markets only - this excludes categorical markets
132
- yes_stms = SeerSubgraphHandler._create_case_variations_condition(
141
+ if market_type == MarketType.SCALAR:
142
+ # Template ID "1" + UP/DOWN outcomes for scalar markets
143
+ and_stms["templateId"] = TemplateId.SCALAR.value
144
+ up_filter = SeerSubgraphHandler._create_case_variations_condition(
133
145
  UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
134
146
  )
135
- no_stms = SeerSubgraphHandler._create_case_variations_condition(
147
+ down_filter = SeerSubgraphHandler._create_case_variations_condition(
136
148
  DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
137
149
  )
138
- elif include_conditional_markets and not include_categorical_markets:
139
- # We are interested in binary markets only
140
- yes_stms = SeerSubgraphHandler._create_case_variations_condition(
150
+ outcome_filters.extend([up_filter, down_filter])
151
+
152
+ elif market_type == MarketType.BINARY:
153
+ # Template ID "2" + YES/NO outcomes for binary markets
154
+ and_stms["templateId"] = TemplateId.CATEGORICAL.value
155
+ yes_filter = SeerSubgraphHandler._create_case_variations_condition(
141
156
  YES_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
142
157
  )
143
- no_stms = SeerSubgraphHandler._create_case_variations_condition(
158
+ no_filter = SeerSubgraphHandler._create_case_variations_condition(
144
159
  NO_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
145
160
  )
161
+ outcome_filters.extend([yes_filter, no_filter])
146
162
 
147
- if (
148
- not include_only_scalar_markets
149
- or include_categorical_markets
150
- or include_conditional_markets
151
- ):
152
- # We should not provide any scalar markets because they are exclusive for categorical markets
153
- exclude_scalar_yes = SeerSubgraphHandler._create_case_variations_condition(
154
- UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_not_contains", "and"
155
- )
156
- exclude_scalar_no = SeerSubgraphHandler._create_case_variations_condition(
157
- DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_not_contains", "and"
163
+ elif market_type == MarketType.CATEGORICAL:
164
+ # Template ID 2 (categorical) OR Template ID 3 (multi-categorical,
165
+ # we treat them as categorical for now for simplicity)
166
+ # https://reality.eth.limo/app/docs/html/contracts.html#templates
167
+ outcome_filters.append(
168
+ {
169
+ "or": [
170
+ {"templateId": TemplateId.CATEGORICAL.value},
171
+ {"templateId": TemplateId.MULTICATEGORICAL.value},
172
+ ]
173
+ }
158
174
  )
159
175
 
160
- where_stms: dict[str, t.Any] = {
161
- "and": [and_stms, yes_stms, no_stms, exclude_scalar_yes, exclude_scalar_no]
162
- }
176
+ # If none specified, don't add any template/outcome filters (returns all types)
177
+
178
+ all_filters = [and_stms] + outcome_filters if and_stms else outcome_filters
179
+ where_stms: dict[str, t.Any] = {"and": all_filters}
163
180
  return where_stms
164
181
 
165
182
  def _build_sort_params(
@@ -194,20 +211,16 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
194
211
  sort_by: SortBy = SortBy.NONE,
195
212
  limit: int | None = None,
196
213
  outcome_supply_gt_if_open: Wei = Wei(0),
197
- include_conditional_markets: bool = True,
198
- include_categorical_markets: bool = True,
199
- include_only_scalar_markets: bool = False,
214
+ market_type: MarketType = MarketType.ALL,
215
+ include_conditional_markets: bool = False,
200
216
  ) -> list[SeerMarket]:
201
217
  sort_direction, sort_by_field = self._build_sort_params(sort_by)
202
218
 
203
- """Returns markets that contain 2 categories plus an invalid outcome."""
204
- # Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option.
205
219
  where_stms = self._build_where_statements(
206
220
  filter_by=filter_by,
207
221
  outcome_supply_gt_if_open=outcome_supply_gt_if_open,
208
222
  include_conditional_markets=include_conditional_markets,
209
- include_categorical_markets=include_categorical_markets,
210
- include_only_scalar_markets=include_only_scalar_markets,
223
+ market_type=market_type,
211
224
  )
212
225
 
213
226
  # These values can not be set to `None`, but they can be omitted.
@@ -19,6 +19,7 @@ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
19
19
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
20
20
  SeerSubgraphHandler,
21
21
  )
22
+ from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
22
23
 
23
24
 
24
25
  class SwapPoolHandler:
@@ -79,7 +80,7 @@ class SwapPoolHandler:
79
80
  amount_out_minimum = self._calculate_amount_out_minimum(
80
81
  amount_wei=amount_wei,
81
82
  token_in=token_in,
82
- price_outcome_token=price_outcome_token,
83
+ price_outcome_token=price_outcome_token.priceOfCollateralInAskingToken,
83
84
  )
84
85
 
85
86
  p = ExactInputSingleParams(
@@ -90,6 +91,15 @@ class SwapPoolHandler:
90
91
  amount_out_minimum=amount_out_minimum,
91
92
  )
92
93
 
94
+ # make sure user has enough tokens to sell
95
+ balance_collateral_token = ContractERC20OnGnosisChain(
96
+ address=token_in
97
+ ).balanceOf(self.api_keys.bet_from_address, web3=web3)
98
+ if balance_collateral_token < amount_wei:
99
+ raise ValueError(
100
+ f"Balance {balance_collateral_token} of {token_in} insufficient for trade, required {amount_wei}"
101
+ )
102
+
93
103
  tx_receipt = SwaprRouterContract().exact_input_single(
94
104
  api_keys=self.api_keys, params=p, web3=web3
95
105
  )
@@ -33,12 +33,7 @@ from cowdao_cowpy.order_book.generated.model import (
33
33
  from eth_account import Account
34
34
  from eth_account.signers.local import LocalAccount
35
35
  from eth_keys.datatypes import PrivateKey as eth_keys_PrivateKey
36
- from tenacity import (
37
- retry_if_not_exception_type,
38
- stop_after_attempt,
39
- wait_exponential,
40
- wait_fixed,
41
- )
36
+ from tenacity import stop_after_attempt, wait_exponential, wait_fixed
42
37
  from web3 import Web3
43
38
 
44
39
  from prediction_market_agent_tooling.config import APIKeys
@@ -54,7 +49,7 @@ from prediction_market_agent_tooling.markets.omen.cow_contracts import (
54
49
  CowGPv2SettlementContract,
55
50
  )
56
51
  from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
57
- from prediction_market_agent_tooling.tools.cow.models import MinimalisticToken, Order
52
+ from prediction_market_agent_tooling.tools.cow.models import MinimalisticTrade, Order
58
53
  from prediction_market_agent_tooling.tools.cow.semaphore import postgres_rate_limited
59
54
  from prediction_market_agent_tooling.tools.utils import utcnow
60
55
 
@@ -108,7 +103,6 @@ def get_sell_token_amount(
108
103
  @tenacity.retry(
109
104
  stop=stop_after_attempt(4),
110
105
  wait=wait_exponential(min=4, max=10),
111
- retry=retry_if_not_exception_type(NoLiquidityAvailableOnCowException),
112
106
  )
113
107
  def get_quote(
114
108
  amount_wei: Wei,
@@ -198,7 +192,7 @@ def handle_allowance(
198
192
  reraise=True,
199
193
  stop=stop_after_attempt(3),
200
194
  wait=wait_fixed(1),
201
- retry=tenacity.retry_if_not_exception_type((TimeoutError, OrderStatusError)),
195
+ retry=tenacity.retry_if_not_exception_type((TimeoutError)),
202
196
  after=lambda x: logger.debug(f"swap_tokens_waiting failed, {x.attempt_number=}."),
203
197
  )
204
198
  def swap_tokens_waiting(
@@ -211,6 +205,7 @@ def swap_tokens_waiting(
211
205
  web3: Web3 | None = None,
212
206
  wait_order_complete: bool = True,
213
207
  timeout: timedelta = timedelta(seconds=120),
208
+ slippage_tolerance: float = 0.01,
214
209
  ) -> tuple[OrderMetaData | None, CompletedOrder]:
215
210
  # CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
216
211
  return asyncio.run(
@@ -224,6 +219,7 @@ def swap_tokens_waiting(
224
219
  timeout=timeout,
225
220
  web3=web3,
226
221
  wait_order_complete=wait_order_complete,
222
+ slippage_tolerance=slippage_tolerance,
227
223
  )
228
224
  )
229
225
 
@@ -353,14 +349,33 @@ async def sign_safe_cow_swap(
353
349
  )
354
350
  def get_trades_by_owner(
355
351
  owner: ChecksumAddress,
356
- ) -> list[MinimalisticToken]:
352
+ ) -> list[MinimalisticTrade]:
357
353
  # Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
358
354
  response = httpx.get(
359
355
  f"https://api.cow.fi/xdai/api/v1/trades",
360
356
  params={"owner": owner},
361
357
  )
362
358
  response.raise_for_status()
363
- return [MinimalisticToken.model_validate(i) for i in response.json()]
359
+ return [MinimalisticTrade.model_validate(i) for i in response.json()]
360
+
361
+
362
+ @tenacity.retry(
363
+ stop=stop_after_attempt(3),
364
+ wait=wait_fixed(1),
365
+ after=lambda x: logger.debug(
366
+ f"get_trades_by_order_uid failed, {x.attempt_number=}."
367
+ ),
368
+ )
369
+ def get_trades_by_order_uid(
370
+ order_uid: HexBytes,
371
+ ) -> list[MinimalisticTrade]:
372
+ # Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
373
+ response = httpx.get(
374
+ f"https://api.cow.fi/xdai/api/v1/trades",
375
+ params={"orderUid": order_uid.hex()},
376
+ )
377
+ response.raise_for_status()
378
+ return [MinimalisticTrade.model_validate(i) for i in response.json()]
364
379
 
365
380
 
366
381
  @tenacity.retry(
@@ -1,14 +1,16 @@
1
1
  from pydantic import BaseModel
2
2
  from sqlmodel import Field, SQLModel
3
3
 
4
- from prediction_market_agent_tooling.gtypes import ChecksumAddress
4
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress, HexBytes
5
5
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
6
6
  from prediction_market_agent_tooling.tools.utils import utcnow
7
7
 
8
8
 
9
- class MinimalisticToken(BaseModel):
9
+ class MinimalisticTrade(BaseModel):
10
10
  sellToken: ChecksumAddress
11
11
  buyToken: ChecksumAddress
12
+ orderUid: HexBytes
13
+ txHash: HexBytes
12
14
 
13
15
 
14
16
  class Order(BaseModel):
@@ -1,14 +1,21 @@
1
1
  import hishel
2
+ import httpx
2
3
 
4
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
3
5
 
4
- class HttpxCachedClient:
5
- def __init__(self) -> None:
6
+ ONE_DAY_IN_SECONDS = 60 * 60 * 24
7
+
8
+
9
+ class HttpxCachedClient(metaclass=SingletonMeta):
10
+ def __init__(self, ttl: int = ONE_DAY_IN_SECONDS) -> None:
6
11
  storage = hishel.FileStorage(
7
- ttl=24 * 60 * 60,
8
- check_ttl_every=1 * 60 * 60,
12
+ ttl=ttl,
13
+ check_ttl_every=60,
9
14
  )
10
15
  controller = hishel.Controller(force_cache=True)
11
- self.client = hishel.CacheClient(storage=storage, controller=controller)
16
+ self.client: httpx.Client = hishel.CacheClient(
17
+ storage=storage, controller=controller
18
+ )
12
19
 
13
- def get_client(self) -> hishel.CacheClient:
20
+ def get_client(self) -> httpx.Client:
14
21
  return self.client
@@ -14,6 +14,9 @@ from prediction_market_agent_tooling.tools.cow.cow_order import (
14
14
  swap_tokens_waiting,
15
15
  )
16
16
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
17
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
18
+ get_slippage_tolerance_per_token,
19
+ )
17
20
  from prediction_market_agent_tooling.tools.tokens.usd import get_usd_in_token
18
21
  from prediction_market_agent_tooling.tools.utils import should_not_happen
19
22
 
@@ -156,10 +159,14 @@ def auto_deposit_erc20(
156
159
  raise ValueError(
157
160
  "Not enough of the source token to sell to get the desired amount of the collateral token."
158
161
  )
162
+ slippage_tolerance = get_slippage_tolerance_per_token(
163
+ KEEPING_ERC20_TOKEN.address, collateral_token_contract.address
164
+ )
159
165
  swap_tokens_waiting(
160
166
  amount_wei=amount_to_sell_wei,
161
167
  sell_token=KEEPING_ERC20_TOKEN.address,
162
168
  buy_token=collateral_token_contract.address,
163
169
  api_keys=api_keys,
164
170
  web3=web3,
171
+ slippage_tolerance=slippage_tolerance,
165
172
  )
@@ -9,6 +9,9 @@ from prediction_market_agent_tooling.tools.contract import (
9
9
  )
10
10
  from prediction_market_agent_tooling.tools.cow.cow_order import swap_tokens_waiting
11
11
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
12
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
13
+ get_slippage_tolerance_per_token,
14
+ )
12
15
  from prediction_market_agent_tooling.tools.utils import should_not_happen
13
16
 
14
17
 
@@ -49,12 +52,17 @@ def auto_withdraw_collateral_token(
49
52
  f"Swapping {amount_wei.as_token} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
50
53
  )
51
54
  # Otherwise, DEX will handle the rest of token swaps.
55
+ slippage_tolerance = get_slippage_tolerance_per_token(
56
+ collateral_token_contract.address,
57
+ KEEPING_ERC20_TOKEN.address,
58
+ )
52
59
  swap_tokens_waiting(
53
60
  amount_wei=amount_wei,
54
61
  sell_token=collateral_token_contract.address,
55
62
  buy_token=KEEPING_ERC20_TOKEN.address,
56
63
  api_keys=api_keys,
57
64
  web3=web3,
65
+ slippage_tolerance=slippage_tolerance,
58
66
  )
59
67
  else:
60
68
  should_not_happen("Unsupported ERC20 contract type.")