prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__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 (88) hide show
  1. prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
  2. prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
  3. prediction_market_agent_tooling/abis/processor.abi.json +16 -0
  4. prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
  5. prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
  6. prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
  7. prediction_market_agent_tooling/benchmark/utils.py +13 -0
  8. prediction_market_agent_tooling/chains.py +1 -0
  9. prediction_market_agent_tooling/config.py +61 -2
  10. prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
  11. prediction_market_agent_tooling/deploy/agent.py +199 -67
  12. prediction_market_agent_tooling/deploy/agent_example.py +1 -1
  13. prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
  14. prediction_market_agent_tooling/deploy/constants.py +6 -0
  15. prediction_market_agent_tooling/gtypes.py +11 -1
  16. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  17. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
  18. prediction_market_agent_tooling/loggers.py +9 -1
  19. prediction_market_agent_tooling/logprobs_parser.py +2 -1
  20. prediction_market_agent_tooling/markets/agent_market.py +106 -18
  21. prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
  22. prediction_market_agent_tooling/markets/data_models.py +120 -7
  23. prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
  24. prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
  25. prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
  26. prediction_market_agent_tooling/markets/market_type.py +74 -0
  27. prediction_market_agent_tooling/markets/markets.py +7 -99
  28. prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
  29. prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
  30. prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
  31. prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
  32. prediction_market_agent_tooling/markets/omen/omen.py +112 -23
  33. prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
  34. prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
  35. prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
  36. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
  37. prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
  38. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  39. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  40. prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
  41. prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
  42. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  43. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
  44. prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
  45. prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
  46. prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
  47. prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
  48. prediction_market_agent_tooling/markets/seer/seer.py +393 -106
  49. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  50. prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
  51. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
  52. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
  53. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
  54. prediction_market_agent_tooling/tools/_generic_value.py +8 -2
  55. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
  56. prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
  57. prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
  58. prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
  59. prediction_market_agent_tooling/tools/contract.py +480 -38
  60. prediction_market_agent_tooling/tools/contract_utils.py +61 -0
  61. prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
  62. prediction_market_agent_tooling/tools/cow/models.py +122 -0
  63. prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
  64. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  65. prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
  66. prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
  67. prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
  68. prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
  69. prediction_market_agent_tooling/tools/openai_utils.py +31 -0
  70. prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
  71. prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
  72. prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
  73. prediction_market_agent_tooling/tools/rephrase.py +71 -0
  74. prediction_market_agent_tooling/tools/singleton.py +11 -6
  75. prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
  76. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
  77. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  78. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  79. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  80. prediction_market_agent_tooling/tools/utils.py +61 -3
  81. prediction_market_agent_tooling/tools/web3_utils.py +63 -9
  82. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
  83. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
  84. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
  85. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
  86. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
  87. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
  88. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,61 @@
1
+ from web3 import Web3
2
+
3
+ from prediction_market_agent_tooling.config import RPCConfig
4
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress
5
+ from prediction_market_agent_tooling.tools.contract import contract_implements_function
6
+
7
+
8
+ def is_erc20_contract(address: ChecksumAddress, web3: Web3 | None = None) -> bool:
9
+ """
10
+ Checks if the given address is an ERC20-compatible contract.
11
+
12
+ It estimates it by looking if it implements symbol, name, decimals, totalSupply, balanceOf.
13
+ """
14
+ web3 = web3 or RPCConfig().get_web3()
15
+ return (
16
+ contract_implements_function(address, "symbol", web3)
17
+ and contract_implements_function(address, "name", web3)
18
+ and contract_implements_function(address, "totalSupply", web3)
19
+ and contract_implements_function(
20
+ address, "balanceOf", web3, function_arg_types=["address"]
21
+ )
22
+ )
23
+
24
+
25
+ def is_nft_contract(address: ChecksumAddress, web3: Web3 | None = None) -> bool:
26
+ """
27
+ Checks if the given address is an NFT-compatible contract (ERC721 or ERC1155).
28
+
29
+ For ERC721, checks for: ownerOf, balanceOf, transferFrom.
30
+ For ERC1155, checks for: balanceOf, safeTransferFrom.
31
+
32
+ Returns True if either ERC721 or ERC1155 interface is detected.
33
+ """
34
+ web3 = web3 or RPCConfig().get_web3()
35
+ is_erc721 = (
36
+ contract_implements_function(
37
+ address, "ownerOf", web3, function_arg_types=["uint256"]
38
+ )
39
+ and contract_implements_function(
40
+ address, "balanceOf", web3, function_arg_types=["address"]
41
+ )
42
+ and contract_implements_function(
43
+ address,
44
+ "transferFrom",
45
+ web3,
46
+ function_arg_types=["address", "address", "uint256"],
47
+ )
48
+ )
49
+ if is_erc721:
50
+ return True
51
+ is_erc1155 = contract_implements_function(
52
+ address, "balanceOf", web3, function_arg_types=["address", "uint256"]
53
+ ) and contract_implements_function(
54
+ address,
55
+ "safeTransferFrom",
56
+ web3,
57
+ function_arg_types=["address", "address", "uint256", "uint256", "bytes"],
58
+ )
59
+ if is_erc1155:
60
+ return True
61
+ return False
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import typing as t
2
3
  from datetime import timedelta
3
4
 
4
5
  import httpx
@@ -8,11 +9,17 @@ from cowdao_cowpy.common.api.errors import UnexpectedResponseError
8
9
  from cowdao_cowpy.common.chains import Chain
9
10
  from cowdao_cowpy.common.config import SupportedChainId
10
11
  from cowdao_cowpy.common.constants import CowContractAddress
12
+ from cowdao_cowpy.contracts.domain import domain
13
+ from cowdao_cowpy.contracts.sign import SigningScheme, sign_order_cancellations
11
14
  from cowdao_cowpy.cow.swap import CompletedOrder, get_order_quote, swap_tokens
12
15
  from cowdao_cowpy.order_book.api import OrderBookApi
13
16
  from cowdao_cowpy.order_book.config import Envs, OrderBookAPIConfigFactory
14
17
  from cowdao_cowpy.order_book.generated.model import (
18
+ UID,
15
19
  Address,
20
+ EcdsaSignature,
21
+ EcdsaSigningScheme,
22
+ OrderCancellations,
16
23
  OrderMetaData,
17
24
  OrderQuoteRequest,
18
25
  OrderQuoteResponse,
@@ -23,28 +30,28 @@ from cowdao_cowpy.order_book.generated.model import (
23
30
  OrderStatus,
24
31
  TokenAmount,
25
32
  )
26
- from cowdao_cowpy.subgraph.client import BaseModel
27
- from tenacity import (
28
- retry_if_not_exception_type,
29
- stop_after_attempt,
30
- wait_exponential,
31
- wait_fixed,
32
- )
33
+ from eth_account import Account
34
+ from eth_account.signers.local import LocalAccount
35
+ from eth_keys.datatypes import PrivateKey as eth_keys_PrivateKey
36
+ from tenacity import stop_after_attempt, wait_exponential, wait_fixed
33
37
  from web3 import Web3
34
38
 
35
39
  from prediction_market_agent_tooling.config import APIKeys
36
- from prediction_market_agent_tooling.gtypes import ChecksumAddress, HexBytes, Wei
40
+ from prediction_market_agent_tooling.gtypes import (
41
+ ChecksumAddress,
42
+ HexBytes,
43
+ HexStr,
44
+ PrivateKey,
45
+ Wei,
46
+ )
37
47
  from prediction_market_agent_tooling.loggers import logger
38
48
  from prediction_market_agent_tooling.markets.omen.cow_contracts import (
39
49
  CowGPv2SettlementContract,
40
50
  )
41
- from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
42
- from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
43
-
44
-
45
- class MinimalisticToken(BaseModel):
46
- sellToken: ChecksumAddress
47
- buyToken: ChecksumAddress
51
+ from prediction_market_agent_tooling.tools.contract import ContractERC20BaseClass
52
+ from prediction_market_agent_tooling.tools.cow.models import MinimalisticTrade, Order
53
+ from prediction_market_agent_tooling.tools.cow.semaphore import postgres_rate_limited
54
+ from prediction_market_agent_tooling.tools.utils import utcnow
48
55
 
49
56
 
50
57
  class OrderStatusError(Exception):
@@ -96,7 +103,6 @@ def get_sell_token_amount(
96
103
  @tenacity.retry(
97
104
  stop=stop_after_attempt(4),
98
105
  wait=wait_exponential(min=4, max=10),
99
- retry=retry_if_not_exception_type(NoLiquidityAvailableOnCowException),
100
106
  )
101
107
  def get_quote(
102
108
  amount_wei: Wei,
@@ -157,10 +163,41 @@ def get_buy_token_amount_else_raise(
157
163
  return Wei(order_quote.quote.buyAmount.root)
158
164
 
159
165
 
166
+ def handle_allowance(
167
+ api_keys: APIKeys,
168
+ sell_token: ChecksumAddress,
169
+ amount_to_check_wei: Wei,
170
+ amount_to_set_wei: Wei | None = None,
171
+ for_address: ChecksumAddress | None = None,
172
+ web3: Web3 | None = None,
173
+ ) -> None:
174
+ # Approve the CoW Swap Vault Relayer to get the sell token only if allowance not sufficient.
175
+ for_address = for_address or Web3.to_checksum_address(
176
+ CowContractAddress.VAULT_RELAYER.value
177
+ )
178
+ current_allowance = ContractERC20BaseClass(address=sell_token).allowance(
179
+ owner=api_keys.bet_from_address,
180
+ for_address=for_address,
181
+ web3=web3,
182
+ )
183
+ if current_allowance < amount_to_check_wei:
184
+ amount_to_set_wei = amount_to_set_wei or amount_to_check_wei
185
+ ContractERC20BaseClass(address=sell_token).approve(
186
+ api_keys,
187
+ for_address=for_address,
188
+ amount_wei=amount_to_set_wei,
189
+ web3=web3,
190
+ )
191
+
192
+
193
+ @postgres_rate_limited(
194
+ api_keys=APIKeys(), rate_id="swap_tokens_waiting", interval_seconds=5.0
195
+ )
160
196
  @tenacity.retry(
197
+ reraise=True,
161
198
  stop=stop_after_attempt(3),
162
199
  wait=wait_fixed(1),
163
- retry=tenacity.retry_if_not_exception_type((TimeoutError, OrderStatusError)),
200
+ retry=tenacity.retry_if_not_exception_type((TimeoutError)),
164
201
  after=lambda x: logger.debug(f"swap_tokens_waiting failed, {x.attempt_number=}."),
165
202
  )
166
203
  def swap_tokens_waiting(
@@ -171,40 +208,37 @@ def swap_tokens_waiting(
171
208
  chain: Chain = Chain.GNOSIS,
172
209
  env: Envs = "prod",
173
210
  web3: Web3 | None = None,
174
- ) -> OrderMetaData:
175
- # Approve the CoW Swap Vault Relayer to get the sell token only if allowance not sufficient.
176
- for_address = Web3.to_checksum_address(CowContractAddress.VAULT_RELAYER.value)
177
- current_allowance = ContractERC20OnGnosisChain(address=sell_token).allowance(
178
- owner=api_keys.bet_from_address,
179
- for_address=for_address,
180
- web3=web3,
181
- )
182
- if current_allowance < amount_wei:
183
- ContractERC20OnGnosisChain(address=sell_token).approve(
184
- api_keys,
185
- for_address=for_address,
186
- amount_wei=amount_wei,
187
- web3=web3,
188
- )
189
-
211
+ wait_order_complete: bool = True,
212
+ timeout: timedelta = timedelta(seconds=120),
213
+ slippage_tolerance: float = 0.01,
214
+ ) -> tuple[OrderMetaData | None, CompletedOrder]:
190
215
  # CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
191
216
  return asyncio.run(
192
217
  swap_tokens_waiting_async(
193
- amount_wei, sell_token, buy_token, api_keys, chain, env
218
+ amount_wei,
219
+ sell_token,
220
+ buy_token,
221
+ api_keys,
222
+ chain,
223
+ env,
224
+ timeout=timeout,
225
+ web3=web3,
226
+ wait_order_complete=wait_order_complete,
227
+ slippage_tolerance=slippage_tolerance,
194
228
  )
195
229
  )
196
230
 
197
231
 
198
- async def swap_tokens_waiting_async(
232
+ async def place_swap_order(
233
+ api_keys: APIKeys,
199
234
  amount_wei: Wei,
200
235
  sell_token: ChecksumAddress,
201
236
  buy_token: ChecksumAddress,
202
- api_keys: APIKeys,
203
237
  chain: Chain,
204
238
  env: Envs,
205
- timeout: timedelta = timedelta(seconds=120),
206
239
  slippage_tolerance: float = 0.01,
207
- ) -> OrderMetaData:
240
+ valid_to: int | None = None,
241
+ ) -> CompletedOrder:
208
242
  account = api_keys.get_account()
209
243
  safe_address = api_keys.safe_address_checksum
210
244
 
@@ -217,6 +251,7 @@ async def swap_tokens_waiting_async(
217
251
  chain=chain,
218
252
  env=env,
219
253
  slippage_tolerance=slippage_tolerance,
254
+ valid_to=valid_to,
220
255
  )
221
256
  logger.info(f"Order created: {order}")
222
257
 
@@ -224,6 +259,12 @@ async def swap_tokens_waiting_async(
224
259
  logger.info(f"Safe is used. Signing the order after its creation.")
225
260
  await sign_safe_cow_swap(api_keys, order, chain, env)
226
261
 
262
+ return order
263
+
264
+
265
+ async def wait_for_order_completion(
266
+ order: CompletedOrder, timeout: timedelta = timedelta(seconds=120)
267
+ ) -> OrderMetaData:
227
268
  start_time = utcnow()
228
269
 
229
270
  while True:
@@ -253,6 +294,41 @@ async def swap_tokens_waiting_async(
253
294
  await asyncio.sleep(3.14)
254
295
 
255
296
 
297
+ async def swap_tokens_waiting_async(
298
+ amount_wei: Wei,
299
+ sell_token: ChecksumAddress,
300
+ buy_token: ChecksumAddress,
301
+ api_keys: APIKeys,
302
+ chain: Chain,
303
+ env: Envs,
304
+ timeout: timedelta = timedelta(seconds=120),
305
+ slippage_tolerance: float = 0.01,
306
+ web3: Web3 | None = None,
307
+ wait_order_complete: bool = True,
308
+ ) -> tuple[OrderMetaData | None, CompletedOrder]:
309
+ handle_allowance(
310
+ api_keys=api_keys,
311
+ sell_token=sell_token,
312
+ amount_to_check_wei=amount_wei,
313
+ web3=web3,
314
+ )
315
+ valid_to = (utcnow() + timeout).timestamp()
316
+ order = await place_swap_order(
317
+ api_keys=api_keys,
318
+ amount_wei=amount_wei,
319
+ sell_token=sell_token,
320
+ buy_token=buy_token,
321
+ chain=chain,
322
+ env=env,
323
+ slippage_tolerance=slippage_tolerance,
324
+ valid_to=int(valid_to),
325
+ )
326
+ if wait_order_complete:
327
+ order_metadata = await wait_for_order_completion(order=order, timeout=timeout)
328
+ return order_metadata, order
329
+ return None, order
330
+
331
+
256
332
  @tenacity.retry(
257
333
  stop=tenacity.stop_after_attempt(3),
258
334
  wait=tenacity.wait_fixed(1),
@@ -266,11 +342,8 @@ async def sign_safe_cow_swap(
266
342
  ) -> None:
267
343
  order_book_api = get_order_book_api(env, chain)
268
344
  posted_order = await order_book_api.get_order_by_uid(order.uid)
269
- CowGPv2SettlementContract(
270
- address=Web3.to_checksum_address(
271
- check_not_none(posted_order.settlementContract).root
272
- )
273
- ).setPreSignature(
345
+
346
+ CowGPv2SettlementContract().setPreSignature(
274
347
  api_keys,
275
348
  HexBytes(posted_order.uid.root),
276
349
  True,
@@ -284,11 +357,111 @@ async def sign_safe_cow_swap(
284
357
  )
285
358
  def get_trades_by_owner(
286
359
  owner: ChecksumAddress,
287
- ) -> list[MinimalisticToken]:
360
+ ) -> list[MinimalisticTrade]:
288
361
  # Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
289
362
  response = httpx.get(
290
363
  f"https://api.cow.fi/xdai/api/v1/trades",
291
364
  params={"owner": owner},
292
365
  )
293
366
  response.raise_for_status()
294
- return [MinimalisticToken.model_validate(i) for i in response.json()]
367
+ return [MinimalisticTrade.model_validate(i) for i in response.json()]
368
+
369
+
370
+ @tenacity.retry(
371
+ stop=stop_after_attempt(3),
372
+ wait=wait_fixed(1),
373
+ after=lambda x: logger.debug(
374
+ f"get_trades_by_order_uid failed, {x.attempt_number=}."
375
+ ),
376
+ )
377
+ def get_trades_by_order_uid(
378
+ order_uid: HexBytes,
379
+ ) -> list[MinimalisticTrade]:
380
+ # Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
381
+ response = httpx.get(
382
+ f"https://api.cow.fi/xdai/api/v1/trades",
383
+ params={"orderUid": order_uid.to_0x_hex()},
384
+ )
385
+ response.raise_for_status()
386
+ return [MinimalisticTrade.model_validate(i) for i in response.json()]
387
+
388
+
389
+ @tenacity.retry(
390
+ stop=stop_after_attempt(3),
391
+ wait=wait_fixed(1),
392
+ after=lambda x: logger.debug(f"get_orders_by_owner failed, {x.attempt_number=}."),
393
+ )
394
+ def get_orders_by_owner(
395
+ owner: ChecksumAddress,
396
+ ) -> list[Order]:
397
+ """Retrieves all orders with pagination."""
398
+ items = paginate_endpoint(f"https://api.cow.fi/xdai/api/v1/account/{owner}/orders")
399
+ return [Order.model_validate(i) for i in items]
400
+
401
+
402
+ @tenacity.retry(
403
+ stop=stop_after_attempt(3),
404
+ wait=wait_fixed(1),
405
+ after=lambda x: logger.debug(f"get_order_by_uid failed, {x.attempt_number=}."),
406
+ )
407
+ async def get_order_by_uid(
408
+ uid: HexBytes,
409
+ ) -> Order:
410
+ async with httpx.AsyncClient() as client:
411
+ response = await client.get(
412
+ f"https://api.cow.fi/xdai/api/v1/orders/{uid.to_0x_hex()}"
413
+ )
414
+ response.raise_for_status()
415
+ return Order.model_validate(response.json())
416
+
417
+
418
+ @tenacity.retry(
419
+ stop=stop_after_attempt(3),
420
+ wait=wait_fixed(1),
421
+ after=lambda x: logger.debug(f"paginate_endpoint failed, {x.attempt_number=}."),
422
+ )
423
+ def paginate_endpoint(url: str, limit: int = 1000) -> t.Any:
424
+ offset = 0
425
+ results = []
426
+
427
+ while True:
428
+ response = httpx.get(url, params={"offset": offset, "limit": limit})
429
+ response.raise_for_status()
430
+ items = response.json()
431
+ if not items:
432
+ break
433
+
434
+ results.extend(items)
435
+ offset += limit
436
+
437
+ return results
438
+
439
+
440
+ async def cancel_order(
441
+ order_uids: t.Sequence[str],
442
+ api_keys: APIKeys,
443
+ chain: Chain = Chain.GNOSIS,
444
+ env: Envs = "prod",
445
+ ) -> None:
446
+ """Cancels orders that were created by the user corresponding to the api_keys.."""
447
+ order_domain = domain(
448
+ chain=chain, verifying_contract=CowContractAddress.SETTLEMENT_CONTRACT.value
449
+ )
450
+ owner = build_account(api_keys.bet_from_private_key)
451
+ order_signature = sign_order_cancellations(
452
+ order_domain, list(order_uids), owner, SigningScheme.EIP712
453
+ )
454
+ oc = OrderCancellations(
455
+ signature=EcdsaSignature(root=order_signature.data),
456
+ orderUids=[UID(root=order_uid) for order_uid in order_uids],
457
+ signingScheme=EcdsaSigningScheme.eip712,
458
+ )
459
+ order_book_api = get_order_book_api(env, chain)
460
+ await order_book_api.delete_order(orders_cancelation=oc)
461
+
462
+
463
+ def build_account(private_key: PrivateKey) -> LocalAccount:
464
+ return LocalAccount(
465
+ key=eth_keys_PrivateKey(HexBytes(HexStr(private_key.get_secret_value()))),
466
+ account=Account.from_key(private_key.get_secret_value()),
467
+ )
@@ -0,0 +1,122 @@
1
+ from enum import Enum
2
+ from typing import Optional, TypeAlias
3
+
4
+ from pydantic import BaseModel, ConfigDict
5
+ from sqlmodel import Field, SQLModel
6
+
7
+ from prediction_market_agent_tooling.gtypes import (
8
+ HexBytes,
9
+ VerifiedChecksumAddress,
10
+ VerifiedChecksumAddressOrNone,
11
+ Wei,
12
+ )
13
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
14
+ from prediction_market_agent_tooling.tools.utils import utcnow
15
+
16
+
17
+ class MinimalisticTrade(BaseModel):
18
+ sellToken: VerifiedChecksumAddress
19
+ buyToken: VerifiedChecksumAddress
20
+ orderUid: HexBytes
21
+ txHash: HexBytes
22
+
23
+
24
+ class EthFlowData(BaseModel):
25
+ refundTxHash: Optional[HexBytes]
26
+ userValidTo: int
27
+
28
+
29
+ class PlacementError(str, Enum):
30
+ QuoteNotFound = "QuoteNotFound"
31
+ ValidToTooFarInFuture = "ValidToTooFarInFuture"
32
+ PreValidationError = "PreValidationError"
33
+
34
+
35
+ class OnchainOrderData(BaseModel):
36
+ sender: VerifiedChecksumAddress
37
+ placementError: Optional[PlacementError]
38
+
39
+
40
+ class OrderKind(str, Enum):
41
+ buy = "buy"
42
+ sell = "sell"
43
+
44
+
45
+ class SellTokenBalance(str, Enum):
46
+ external = "external"
47
+ internal = "internal"
48
+ erc20 = "erc20"
49
+
50
+
51
+ class BuyTokenBalance(str, Enum):
52
+ internal = "internal"
53
+ erc20 = "erc20"
54
+
55
+
56
+ class SigningScheme(str, Enum):
57
+ eip712 = "eip712"
58
+ ethsign = "ethsign"
59
+ presign = "presign"
60
+ eip1271 = "eip1271"
61
+
62
+
63
+ class OrderClass(str, Enum):
64
+ limit = "limit"
65
+ liquidity = "liquidity"
66
+ market = "market"
67
+
68
+
69
+ class OrderStatus(str, Enum):
70
+ presignaturePending = "presignaturePending"
71
+ open = "open"
72
+ fulfilled = "fulfilled"
73
+ cancelled = "cancelled"
74
+ expired = "expired"
75
+
76
+
77
+ CowOrderUID: TypeAlias = HexBytes
78
+
79
+
80
+ class Order(BaseModel):
81
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
82
+
83
+ uid: CowOrderUID
84
+ quoteId: int | None = None
85
+ validTo: int
86
+ sellAmount: Wei
87
+ sellToken: VerifiedChecksumAddress
88
+ buyAmount: Wei
89
+ buyToken: VerifiedChecksumAddress
90
+ receiver: VerifiedChecksumAddressOrNone
91
+ feeAmount: Wei
92
+ creationDate: DatetimeUTC
93
+ kind: OrderKind
94
+ partiallyFillable: bool
95
+ sellTokenBalance: SellTokenBalance | None
96
+ buyTokenBalance: BuyTokenBalance | None
97
+ signingScheme: SigningScheme
98
+ signature: HexBytes
99
+ from_: VerifiedChecksumAddressOrNone = Field(None, alias="from")
100
+ appData: str
101
+ fullAppData: str | None
102
+ appDataHash: HexBytes | None = None
103
+ class_: str = Field(None, alias="class")
104
+ owner: VerifiedChecksumAddress
105
+ executedSellAmount: Wei
106
+ executedSellAmountBeforeFees: Wei
107
+ executedBuyAmount: Wei
108
+ executedFeeAmount: Wei | None
109
+ invalidated: bool
110
+ status: str
111
+ isLiquidityOrder: bool | None
112
+ ethflowData: EthFlowData | None = None
113
+ onchainUser: VerifiedChecksumAddressOrNone = None
114
+ executedFee: Wei
115
+ executedFeeToken: VerifiedChecksumAddressOrNone
116
+
117
+
118
+ class RateLimit(SQLModel, table=True):
119
+ __tablename__ = "rate_limit"
120
+ __table_args__ = {"extend_existing": True}
121
+ id: str = Field(primary_key=True)
122
+ last_called_at: DatetimeUTC = Field(default_factory=utcnow)
@@ -0,0 +1,104 @@
1
+ import time
2
+ from datetime import timedelta
3
+ from functools import wraps
4
+ from typing import Any, Callable, Optional, TypeVar, cast
5
+
6
+ from sqlalchemy.exc import OperationalError
7
+ from sqlmodel import Session, select
8
+
9
+ from prediction_market_agent_tooling.config import APIKeys
10
+ from prediction_market_agent_tooling.tools.cow.models import RateLimit
11
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
12
+ from prediction_market_agent_tooling.tools.db.db_manager import DBManager
13
+ from prediction_market_agent_tooling.tools.utils import utcnow
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+ FALLBACK_SQL_ENGINE = "sqlite:///rate_limit.db"
18
+
19
+
20
+ def postgres_rate_limited(
21
+ api_keys: APIKeys,
22
+ rate_id: str,
23
+ interval_seconds: float,
24
+ shared_db: bool = False,
25
+ ) -> Callable[[F], F]:
26
+ """rate_id is used to distinguish between different rate limits for different functions"""
27
+ limiter = RateLimiter(id=rate_id, interval_seconds=interval_seconds)
28
+
29
+ def decorator(func: F) -> F:
30
+ @wraps(func)
31
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
32
+ sqlalchemy_db_url = (
33
+ api_keys.sqlalchemy_db_url.get_secret_value()
34
+ if shared_db
35
+ else FALLBACK_SQL_ENGINE
36
+ )
37
+
38
+ db_manager = DBManager(sqlalchemy_db_url)
39
+ db_manager.create_tables([RateLimit])
40
+
41
+ with db_manager.get_session() as session:
42
+ limiter.enforce(session)
43
+ return func(*args, **kwargs)
44
+
45
+ return cast(F, wrapper)
46
+
47
+ return decorator
48
+
49
+
50
+ class RateLimiter:
51
+ def __init__(self, id: str, interval_seconds: float = 1.0) -> None:
52
+ self.id = id
53
+ self.interval = timedelta(seconds=interval_seconds)
54
+
55
+ def enforce(self, session: Session, timeout_seconds: float = 30.0) -> None:
56
+ """
57
+ Enforces the rate limit inside a transaction.
58
+ Blocks until allowed or timeout is reached.
59
+
60
+ Args:
61
+ session: The database session to use
62
+ timeout_seconds: Maximum time in seconds to wait before giving up
63
+
64
+ Raises:
65
+ TimeoutError: If the rate limit cannot be acquired within the timeout period
66
+ """
67
+ start_time = time.monotonic()
68
+
69
+ while True:
70
+ try:
71
+ with session.begin():
72
+ stmt = (
73
+ select(RateLimit)
74
+ .where(RateLimit.id == self.id)
75
+ .with_for_update()
76
+ )
77
+ result: Optional[RateLimit] = session.exec(stmt).first()
78
+
79
+ now = utcnow()
80
+
81
+ if result is None:
82
+ # First time this limiter is used
83
+ session.add(RateLimit(id=self.id))
84
+ return
85
+
86
+ last_called_aware = DatetimeUTC.from_datetime(result.last_called_at)
87
+ elapsed = now - last_called_aware
88
+ if elapsed >= self.interval:
89
+ result.last_called_at = now
90
+ session.add(result)
91
+ return
92
+
93
+ # Not enough time passed, sleep and retry
94
+ to_sleep = (self.interval - elapsed).total_seconds()
95
+ time.sleep(to_sleep)
96
+ except OperationalError:
97
+ # Backoff if DB is under contention
98
+ elapsed_time = time.monotonic() - start_time
99
+ if elapsed_time > timeout_seconds:
100
+ raise TimeoutError(
101
+ f"Could not acquire rate limit '{self.id}' "
102
+ f"after {elapsed_time:.1f} seconds due to database contention"
103
+ )
104
+ time.sleep(0.5)
@@ -34,8 +34,20 @@ class DatetimeUTC(datetime):
34
34
  def __get_pydantic_core_schema__(
35
35
  cls, source_type: t.Any, handler: GetCoreSchemaHandler
36
36
  ) -> CoreSchema:
37
- dt_schema = handler(datetime)
38
- return core_schema.no_info_after_validator_function(cls._validate, dt_schema)
37
+ # Use union schema to handle int, str, and datetime inputs directly
38
+ return core_schema.union_schema(
39
+ [
40
+ core_schema.no_info_after_validator_function(
41
+ cls._validate, core_schema.int_schema()
42
+ ),
43
+ core_schema.no_info_after_validator_function(
44
+ cls._validate, core_schema.str_schema()
45
+ ),
46
+ core_schema.no_info_after_validator_function(
47
+ cls._validate, handler(datetime)
48
+ ),
49
+ ]
50
+ )
39
51
 
40
52
  @staticmethod
41
53
  def from_datetime(dt: datetime) -> "DatetimeUTC":