prediction-market-agent-tooling 0.17.0__tar.gz → 0.19.0__tar.gz

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 (71) hide show
  1. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/PKG-INFO +2 -1
  2. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/config.py +32 -0
  3. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/agent.py +33 -4
  4. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/markets.py +43 -1
  5. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/omen.py +32 -16
  6. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/omen_contracts.py +44 -32
  7. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/omen_resolving.py +14 -4
  8. prediction_market_agent_tooling-0.19.0/prediction_market_agent_tooling/monitor/langfuse/langfuse_wrapper.py +26 -0
  9. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/monitor.py +9 -5
  10. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/balances.py +8 -4
  11. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/contract.py +17 -0
  12. prediction_market_agent_tooling-0.19.0/prediction_market_agent_tooling/tools/gnosis_rpc.py +6 -0
  13. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/web3_utils.py +64 -14
  14. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/pyproject.toml +2 -1
  15. prediction_market_agent_tooling-0.17.0/prediction_market_agent_tooling/tools/gnosis_rpc.py +0 -24
  16. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/LICENSE +0 -0
  17. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/README.md +0 -0
  18. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/erc20.abi.json +0 -0
  19. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_dxdao.abi.json +0 -0
  20. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_fpmm.abi.json +0 -0
  21. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_fpmm_conditionaltokens.abi.json +0 -0
  22. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_fpmm_factory.abi.json +0 -0
  23. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_kleros.abi.json +0 -0
  24. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_oracle.abi.json +0 -0
  25. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/omen_realitio.abi.json +0 -0
  26. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/abis/wxdai.abi.json +0 -0
  27. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/benchmark/__init__.py +0 -0
  28. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/benchmark/agents.py +0 -0
  29. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/benchmark/benchmark.py +0 -0
  30. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/benchmark/utils.py +0 -0
  31. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/agent_example.py +0 -0
  32. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/constants.py +0 -0
  33. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/gcp/deploy.py +0 -0
  34. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/gcp/kubernetes_models.py +0 -0
  35. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/deploy/gcp/utils.py +0 -0
  36. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/gtypes.py +0 -0
  37. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/agent_market.py +0 -0
  38. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/categorize.py +0 -0
  39. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/data_models.py +0 -0
  40. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/manifold/__init__.py +0 -0
  41. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/manifold/api.py +0 -0
  42. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/manifold/data_models.py +0 -0
  43. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/manifold/manifold.py +0 -0
  44. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/manifold/utils.py +0 -0
  45. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/__init__.py +0 -0
  46. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/data_models.py +0 -0
  47. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +0 -0
  48. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/polymarket/api.py +0 -0
  49. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/polymarket/data_models.py +0 -0
  50. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -0
  51. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/polymarket/polymarket.py +0 -0
  52. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/markets/polymarket/utils.py +0 -0
  53. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/markets/manifold.py +0 -0
  54. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/markets/omen.py +0 -0
  55. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/markets/polymarket.py +0 -0
  56. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/monitor_app.py +0 -0
  57. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/monitor/monitor_settings.py +0 -0
  58. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/py.typed +0 -0
  59. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +0 -0
  60. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +0 -0
  61. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/betting_strategies/minimum_bet_to_win.py +0 -0
  62. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/betting_strategies/stretch_bet_between.py +0 -0
  63. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/cache.py +0 -0
  64. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/costs.py +0 -0
  65. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/google.py +0 -0
  66. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/hexbytes_custom.py +0 -0
  67. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/is_predictable.py +0 -0
  68. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/parallelism.py +0 -0
  69. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/safe.py +0 -0
  70. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/singleton.py +0 -0
  71. {prediction_market_agent_tooling-0.17.0 → prediction_market_agent_tooling-0.19.0}/prediction_market_agent_tooling/tools/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.17.0
3
+ Version: 0.19.0
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.12
@@ -22,6 +22,7 @@ Requires-Dist: isort (>=5.13.2,<6.0.0)
22
22
  Requires-Dist: langchain (>=0.1.9,<0.2.0) ; extra == "langchain"
23
23
  Requires-Dist: langchain-community (>=0.0.19)
24
24
  Requires-Dist: langchain-openai (>=0.0.5,<0.0.6) ; extra == "langchain"
25
+ Requires-Dist: langfuse (>=2.27.1,<3.0.0)
25
26
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
26
27
  Requires-Dist: mech-client (>=0.2.13,<0.3.0)
27
28
  Requires-Dist: numpy (>=1.26.4,<2.0.0)
@@ -1,5 +1,7 @@
1
1
  import typing as t
2
2
 
3
+ from gnosis.eth import EthereumClient
4
+ from gnosis.safe import Safe
3
5
  from pydantic import BaseModel
4
6
  from pydantic.types import SecretStr
5
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -30,6 +32,10 @@ class APIKeys(BaseSettings):
30
32
  GOOGLE_SEARCH_API_KEY: t.Optional[SecretStr] = None
31
33
  GOOGLE_SEARCH_ENGINE_ID: t.Optional[SecretStr] = None
32
34
 
35
+ LANGFUSE_SECRET_KEY: t.Optional[SecretStr] = None
36
+ LANGFUSE_PUBLIC_KEY: t.Optional[SecretStr] = None
37
+ LANGFUSE_HOST: t.Optional[str] = None
38
+
33
39
  ENABLE_CACHE: bool = True
34
40
  CACHE_DIR: str = "./.cache"
35
41
 
@@ -72,6 +78,24 @@ class APIKeys(BaseSettings):
72
78
  "GOOGLE_SEARCH_ENGINE_ID missing in the environment.",
73
79
  )
74
80
 
81
+ @property
82
+ def langfuse_secret_key(self) -> SecretStr:
83
+ return check_not_none(
84
+ self.LANGFUSE_SECRET_KEY, "LANGFUSE_SECRET_KEY missing in the environment."
85
+ )
86
+
87
+ @property
88
+ def langfuse_public_key(self) -> SecretStr:
89
+ return check_not_none(
90
+ self.LANGFUSE_PUBLIC_KEY, "LANGFUSE_PUBLIC_KEY missing in the environment."
91
+ )
92
+
93
+ @property
94
+ def langfuse_host(self) -> str:
95
+ return check_not_none(
96
+ self.LANGFUSE_HOST, "LANGFUSE_HOST missing in the environment."
97
+ )
98
+
75
99
  def model_dump_public(self) -> dict[str, t.Any]:
76
100
  return {
77
101
  k: v
@@ -110,3 +134,11 @@ class PrivateCredentials(BaseModel):
110
134
  private_key=api_keys.bet_from_private_key,
111
135
  safe_address=api_keys.SAFE_ADDRESS,
112
136
  )
137
+
138
+ def check_if_is_safe_owner(self, ethereum_client: EthereumClient) -> bool:
139
+ if not self.safe_address:
140
+ raise ValueError("Cannot check ownership if safe_address is not defined.")
141
+
142
+ s = Safe(self.safe_address, ethereum_client) # type: ignore[abstract]
143
+ public_key_from_signer = private_key_to_public_key(self.private_key)
144
+ return s.retrieve_is_owner(public_key_from_signer)
@@ -3,7 +3,7 @@ import os
3
3
  import tempfile
4
4
  import time
5
5
  import typing as t
6
- from datetime import datetime
6
+ from datetime import datetime, timedelta
7
7
 
8
8
  from loguru import logger
9
9
  from pydantic import BaseModel, BeforeValidator
@@ -30,13 +30,20 @@ from prediction_market_agent_tooling.markets.agent_market import (
30
30
  SortBy,
31
31
  )
32
32
  from prediction_market_agent_tooling.markets.data_models import BetAmount
33
- from prediction_market_agent_tooling.markets.markets import MarketType
33
+ from prediction_market_agent_tooling.markets.markets import (
34
+ MarketType,
35
+ have_bet_on_market_since,
36
+ )
34
37
  from prediction_market_agent_tooling.markets.omen.omen import (
35
38
  redeem_from_all_user_positions,
36
39
  )
40
+ from prediction_market_agent_tooling.monitor.langfuse.langfuse_wrapper import (
41
+ LangfuseWrapper,
42
+ )
37
43
  from prediction_market_agent_tooling.monitor.monitor_app import (
38
44
  MARKET_TYPE_TO_DEPLOYED_AGENT,
39
45
  )
46
+ from prediction_market_agent_tooling.tools.is_predictable import is_predictable_binary
40
47
  from prediction_market_agent_tooling.tools.utils import DatetimeWithTimezone, utcnow
41
48
 
42
49
  MAX_AVAILABLE_MARKETS = 20
@@ -77,7 +84,10 @@ class Answer(BaseModel):
77
84
 
78
85
 
79
86
  class DeployableAgent:
87
+ bet_on_n_markets_per_run: int = 1
88
+
80
89
  def __init__(self) -> None:
90
+ self.langfuse_wrapper = LangfuseWrapper(agent_name=self.__class__.__name__)
81
91
  self.load()
82
92
 
83
93
  def __init_subclass__(cls, **kwargs: t.Any) -> None:
@@ -89,11 +99,30 @@ class DeployableAgent:
89
99
  def load(self) -> None:
90
100
  pass
91
101
 
102
+ def have_bet_on_market_since(self, market: AgentMarket, since: timedelta) -> bool:
103
+ return have_bet_on_market_since(keys=APIKeys(), market=market, since=since)
104
+
92
105
  def pick_markets(self, markets: t.Sequence[AgentMarket]) -> t.Sequence[AgentMarket]:
93
106
  """
94
- This method should be implemented by the subclass to pick the markets to bet on. By default, it picks only the first market.
107
+ Subclasses can implement their own logic instead of this one, or on top of this one.
108
+ By default, it picks only the first {n_markets_per_run} markets where user didn't bet recently and it's a reasonable question.
95
109
  """
96
- return markets[:1]
110
+ picked: list[AgentMarket] = []
111
+
112
+ for market in markets:
113
+ if len(picked) >= self.bet_on_n_markets_per_run:
114
+ break
115
+
116
+ if self.have_bet_on_market_since(market, since=timedelta(hours=24)):
117
+ continue
118
+
119
+ # Do as a last check, as it uses paid OpenAI API.
120
+ if not is_predictable_binary(market.question):
121
+ continue
122
+
123
+ picked.append(market)
124
+
125
+ return picked
97
126
 
98
127
  def answer_binary_market(self, market: AgentMarket) -> Answer | None:
99
128
  """
@@ -1,19 +1,29 @@
1
1
  import typing as t
2
- from datetime import datetime
2
+ from datetime import datetime, timedelta
3
3
  from enum import Enum
4
4
 
5
+ from prediction_market_agent_tooling.config import APIKeys, PrivateCredentials
5
6
  from prediction_market_agent_tooling.markets.agent_market import (
6
7
  AgentMarket,
7
8
  FilterBy,
8
9
  SortBy,
9
10
  )
11
+ from prediction_market_agent_tooling.markets.manifold.api import (
12
+ get_authenticated_user,
13
+ get_manifold_bets,
14
+ get_manifold_market,
15
+ )
10
16
  from prediction_market_agent_tooling.markets.manifold.manifold import (
11
17
  ManifoldAgentMarket,
12
18
  )
13
19
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
20
+ from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
21
+ OmenSubgraphHandler,
22
+ )
14
23
  from prediction_market_agent_tooling.markets.polymarket.polymarket import (
15
24
  PolymarketAgentMarket,
16
25
  )
26
+ from prediction_market_agent_tooling.tools.utils import should_not_happen, utcnow
17
27
 
18
28
 
19
29
  class MarketType(str, Enum):
@@ -52,3 +62,35 @@ def get_binary_markets(
52
62
  excluded_questions=excluded_questions,
53
63
  )
54
64
  return markets
65
+
66
+
67
+ def have_bet_on_market_since(
68
+ keys: APIKeys, market: AgentMarket, since: timedelta
69
+ ) -> bool:
70
+ start_time = utcnow() - since
71
+ credentials = PrivateCredentials.from_api_keys(keys)
72
+ recently_betted_questions = (
73
+ set(
74
+ get_manifold_market(b.contractId).question
75
+ for b in get_manifold_bets(
76
+ user_id=get_authenticated_user(
77
+ keys.manifold_api_key.get_secret_value()
78
+ ).id,
79
+ start_time=start_time,
80
+ end_time=None,
81
+ )
82
+ )
83
+ if isinstance(market, ManifoldAgentMarket)
84
+ else (
85
+ set(
86
+ b.title
87
+ for b in OmenSubgraphHandler().get_bets(
88
+ better_address=credentials.public_key,
89
+ start_time=start_time,
90
+ )
91
+ )
92
+ if isinstance(market, OmenAgentMarket)
93
+ else should_not_happen(f"Uknown market: {market}")
94
+ )
95
+ )
96
+ return market.question in recently_betted_questions
@@ -432,6 +432,7 @@ def omen_sell_outcome_tx(
432
432
  market: OmenAgentMarket,
433
433
  outcome: str,
434
434
  auto_withdraw: bool,
435
+ web3: Web3 | None = None,
435
436
  ) -> None:
436
437
  """
437
438
  Sells the given xDai value of shares corresponding to the given outcome in
@@ -447,7 +448,10 @@ def omen_sell_outcome_tx(
447
448
  collateral_token = OmenCollateralTokenContract()
448
449
 
449
450
  # Verify, that markets uses conditional tokens that we expect.
450
- if market_contract.conditionalTokens() != conditional_token_contract.address:
451
+ if (
452
+ market_contract.conditionalTokens(web3=web3)
453
+ != conditional_token_contract.address
454
+ ):
451
455
  raise ValueError(
452
456
  f"Market {market.id} uses conditional token that we didn't expect, {market_contract.conditionalTokens()} != {conditional_token_contract.address=}"
453
457
  )
@@ -457,7 +461,7 @@ def omen_sell_outcome_tx(
457
461
 
458
462
  # Calculate the amount of shares we will sell for the given selling amount of xdai.
459
463
  max_outcome_tokens_to_sell = market_contract.calcSellAmount(
460
- amount_wei, outcome_index
464
+ amount_wei, outcome_index, web3=web3
461
465
  )
462
466
  # Allow 1% slippage.
463
467
  max_outcome_tokens_to_sell = add_fraction(max_outcome_tokens_to_sell, 0.01)
@@ -467,6 +471,7 @@ def omen_sell_outcome_tx(
467
471
  private_credentials=private_credentials,
468
472
  for_address=market_contract.address,
469
473
  approve=True,
474
+ web3=web3,
470
475
  )
471
476
  # Sell the shares.
472
477
  market_contract.sell(
@@ -474,12 +479,12 @@ def omen_sell_outcome_tx(
474
479
  amount_wei,
475
480
  outcome_index,
476
481
  max_outcome_tokens_to_sell,
482
+ web3=web3,
477
483
  )
478
484
  if auto_withdraw:
479
485
  # Optionally, withdraw from the collateral token back to the `from_address` wallet.
480
486
  collateral_token.withdraw(
481
- private_credentials=private_credentials,
482
- amount_wei=amount_wei,
487
+ private_credentials=private_credentials, amount_wei=amount_wei, web3=web3
483
488
  )
484
489
 
485
490
 
@@ -489,6 +494,7 @@ def binary_omen_sell_outcome_tx(
489
494
  market: OmenAgentMarket,
490
495
  binary_outcome: bool,
491
496
  auto_withdraw: bool,
497
+ web3: Web3 | None = None,
492
498
  ) -> None:
493
499
  omen_sell_outcome_tx(
494
500
  private_credentials=private_credentials,
@@ -496,6 +502,7 @@ def binary_omen_sell_outcome_tx(
496
502
  market=market,
497
503
  outcome=OMEN_TRUE_OUTCOME if binary_outcome else OMEN_FALSE_OUTCOME,
498
504
  auto_withdraw=auto_withdraw,
505
+ web3=web3,
499
506
  )
500
507
 
501
508
 
@@ -509,6 +516,7 @@ def omen_create_market_tx(
509
516
  outcomes: list[str],
510
517
  auto_deposit: bool,
511
518
  fee: float = OMEN_DEFAULT_MARKET_FEE,
519
+ web3: Web3 | None = None,
512
520
  ) -> ChecksumAddress:
513
521
  """
514
522
  Based on omen-exchange TypeScript code: https://github.com/protofire/omen-exchange/blob/b0b9a3e71b415d6becf21fe428e1c4fc0dad2e80/app/src/services/cpk/cpk.ts#L308
@@ -539,19 +547,22 @@ def omen_create_market_tx(
539
547
  private_credentials=private_credentials,
540
548
  for_address=factory_contract.address,
541
549
  amount_wei=initial_funds_wei,
550
+ web3=web3,
542
551
  )
543
552
 
544
553
  # Deposit xDai to the collateral token,
545
554
  # this can be skipped, if we know we already have enough collateral tokens.
546
555
  collateral_token_balance = collateral_token_contract.balanceOf(
547
- for_address=from_address,
556
+ for_address=from_address, web3=web3
548
557
  )
549
558
  if (
550
559
  auto_deposit
551
560
  and initial_funds_wei > 0
552
561
  and collateral_token_balance < initial_funds_wei
553
562
  ):
554
- collateral_token_contract.deposit(private_credentials, initial_funds_wei)
563
+ collateral_token_contract.deposit(
564
+ private_credentials, initial_funds_wei, web3=web3
565
+ )
555
566
 
556
567
  # Create the question on Realitio.
557
568
  question_id = realitio_contract.askQuestion(
@@ -562,6 +573,7 @@ def omen_create_market_tx(
562
573
  language=language,
563
574
  arbitrator=Arbitrator.KLEROS,
564
575
  opening=closing_time, # The question is opened at the closing time of the market.
576
+ web3=web3,
565
577
  )
566
578
 
567
579
  # Construct the condition id.
@@ -569,13 +581,15 @@ def omen_create_market_tx(
569
581
  question_id=question_id,
570
582
  oracle_address=oracle_contract.address,
571
583
  outcomes_slot_count=len(outcomes),
584
+ web3=web3,
572
585
  )
573
- if not conditional_token_contract.does_condition_exists(condition_id):
586
+ if not conditional_token_contract.does_condition_exists(condition_id, web3=web3):
574
587
  conditional_token_contract.prepareCondition(
575
588
  private_credentials=private_credentials,
576
589
  question_id=question_id,
577
590
  oracle_address=oracle_contract.address,
578
591
  outcomes_slot_count=len(outcomes),
592
+ web3=web3,
579
593
  )
580
594
 
581
595
  # Create the market.
@@ -584,6 +598,7 @@ def omen_create_market_tx(
584
598
  condition_id=condition_id,
585
599
  fee=fee,
586
600
  initial_funds_wei=initial_funds_wei,
601
+ web3=web3,
587
602
  )
588
603
 
589
604
  # Note: In the Omen's Typescript code, there is futher a creation of `stakingRewardsFactoryAddress`,
@@ -602,6 +617,7 @@ def omen_fund_market_tx(
602
617
  market: OmenAgentMarket,
603
618
  funds: Wei,
604
619
  auto_deposit: bool,
620
+ web3: Web3 | None = None,
605
621
  ) -> None:
606
622
  from_address = private_credentials.public_key
607
623
  market_contract = market.get_contract()
@@ -611,20 +627,19 @@ def omen_fund_market_tx(
611
627
  # this can be skipped, if we know we already have enough collateral tokens.
612
628
  if (
613
629
  auto_deposit
614
- and collateral_token_contract.balanceOf(
615
- for_address=from_address,
616
- )
630
+ and collateral_token_contract.balanceOf(for_address=from_address, web3=web3)
617
631
  < funds
618
632
  ):
619
- collateral_token_contract.deposit(private_credentials, funds)
633
+ collateral_token_contract.deposit(private_credentials, funds, web3=web3)
620
634
 
621
635
  collateral_token_contract.approve(
622
636
  private_credentials=private_credentials,
623
637
  for_address=market_contract.address,
624
638
  amount_wei=funds,
639
+ web3=web3,
625
640
  )
626
641
 
627
- market_contract.addFunding(private_credentials, funds)
642
+ market_contract.addFunding(private_credentials, funds, web3=web3)
628
643
 
629
644
 
630
645
  def build_parent_collection_id() -> HexStr:
@@ -727,7 +742,7 @@ def omen_remove_fund_market_tx(
727
742
  """
728
743
  from_address = private_credentials.public_key
729
744
  market_contract = market.get_contract()
730
- original_balances = get_balances(from_address)
745
+ original_balances = get_balances(from_address, web3=web3)
731
746
 
732
747
  total_shares = market_contract.balanceOf(from_address, web3=web3)
733
748
  if total_shares == 0:
@@ -763,7 +778,8 @@ def omen_remove_fund_market_tx(
763
778
  amount=amount_to_merge,
764
779
  web3=web3,
765
780
  )
766
- new_balances = get_balances(from_address)
781
+
782
+ new_balances = get_balances(from_address, web3)
767
783
 
768
784
  logger.debug(f"Result from merge positions {result}")
769
785
  logger.info(
@@ -800,7 +816,7 @@ def redeem_from_all_user_positions(
800
816
  f"[{index+1} / {len(user_positions)}] Processing redeem from {user_position.id=}."
801
817
  )
802
818
 
803
- original_balances = get_balances(public_key)
819
+ original_balances = get_balances(public_key, web3)
804
820
  conditional_token_contract.redeemPositions(
805
821
  private_credentials=private_credentials,
806
822
  collateral_token_address=user_position.position.collateral_token_contract_address_checksummed,
@@ -809,7 +825,7 @@ def redeem_from_all_user_positions(
809
825
  index_sets=user_position.position.indexSets,
810
826
  web3=web3,
811
827
  )
812
- new_balances = get_balances(public_key)
828
+ new_balances = get_balances(public_key, web3)
813
829
 
814
830
  logger.info(
815
831
  f"Redeemed {new_balances.wxdai - original_balances.wxdai} wxDai from position {user_position.id=}."
@@ -56,6 +56,7 @@ class OmenOracleContract(ContractOnGnosisChain):
56
56
  template_id: int,
57
57
  question_raw: str,
58
58
  n_outcomes: int,
59
+ web3: Web3 | None = None,
59
60
  ) -> TxReceipt:
60
61
  return self.send(
61
62
  private_credentials=private_credentials,
@@ -66,6 +67,7 @@ class OmenOracleContract(ContractOnGnosisChain):
66
67
  question=question_raw,
67
68
  numOutcomes=n_outcomes,
68
69
  ),
70
+ web3=web3,
69
71
  )
70
72
 
71
73
 
@@ -86,11 +88,13 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
86
88
  question_id: HexBytes,
87
89
  oracle_address: ChecksumAddress,
88
90
  outcomes_slot_count: int,
91
+ web3: Web3 | None = None,
89
92
  ) -> HexBytes:
90
93
  id_ = HexBytes(
91
94
  self.call(
92
95
  "getConditionId",
93
96
  [oracle_address, question_id, outcomes_slot_count],
97
+ web3=web3,
94
98
  )
95
99
  )
96
100
  return id_
@@ -177,34 +181,29 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
177
181
  )
178
182
 
179
183
  def getOutcomeSlotCount(
180
- self,
181
- condition_id: HexBytes,
184
+ self, condition_id: HexBytes, web3: Web3 | None = None
182
185
  ) -> int:
183
- count: int = self.call(
184
- "getOutcomeSlotCount",
185
- [condition_id],
186
- )
186
+ count: int = self.call("getOutcomeSlotCount", [condition_id], web3=web3)
187
187
  return count
188
188
 
189
189
  def does_condition_exists(
190
- self,
191
- condition_id: HexBytes,
190
+ self, condition_id: HexBytes, web3: Web3 | None = None
192
191
  ) -> bool:
193
- return self.getOutcomeSlotCount(condition_id) > 0
192
+ return self.getOutcomeSlotCount(condition_id, web3=web3) > 0
194
193
 
195
194
  def is_condition_resolved(
196
- self,
197
- condition_id: HexBytes,
195
+ self, condition_id: HexBytes, web3: Web3 | None = None
198
196
  ) -> bool:
199
197
  # from ConditionalTokens.redeemPositions:
200
198
  # uint den = payoutDenominator[conditionId]; require(den > 0, "result for condition not received yet");
201
- payout_for_condition = self.payoutDenominator(condition_id)
199
+ payout_for_condition = self.payoutDenominator(condition_id, web3=web3)
202
200
  return payout_for_condition > 0
203
201
 
204
- def payoutDenominator(self, condition_id: HexBytes) -> int:
202
+ def payoutDenominator(
203
+ self, condition_id: HexBytes, web3: Web3 | None = None
204
+ ) -> int:
205
205
  payoutForCondition: int = self.call(
206
- "payoutDenominator",
207
- [condition_id],
206
+ "payoutDenominator", [condition_id], web3=web3
208
207
  )
209
208
  return payoutForCondition
210
209
 
@@ -214,6 +213,7 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
214
213
  for_address: ChecksumAddress,
215
214
  approve: bool,
216
215
  tx_params: t.Optional[TxParams] = None,
216
+ web3: Web3 | None = None,
217
217
  ) -> TxReceipt:
218
218
  return self.send(
219
219
  private_credentials=private_credentials,
@@ -223,6 +223,7 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
223
223
  approve,
224
224
  ],
225
225
  tx_params=tx_params,
226
+ web3=web3,
226
227
  )
227
228
 
228
229
  def prepareCondition(
@@ -232,6 +233,7 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
232
233
  question_id: HexBytes,
233
234
  outcomes_slot_count: int,
234
235
  tx_params: t.Optional[TxParams] = None,
236
+ web3: Web3 | None = None,
235
237
  ) -> TxReceipt:
236
238
  return self.send(
237
239
  private_credentials=private_credentials,
@@ -242,6 +244,7 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
242
244
  outcomes_slot_count,
243
245
  ],
244
246
  tx_params=tx_params,
247
+ web3=web3,
245
248
  )
246
249
 
247
250
 
@@ -272,25 +275,18 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
272
275
  return calculated_shares
273
276
 
274
277
  def calcSellAmount(
275
- self,
276
- return_amount: Wei,
277
- outcome_index: int,
278
+ self, return_amount: Wei, outcome_index: int, web3: Web3 | None = None
278
279
  ) -> OmenOutcomeToken:
279
280
  """
280
281
  Returns amount of shares we will sell for the requested wei.
281
282
  """
282
283
  calculated_shares: OmenOutcomeToken = self.call(
283
- "calcSellAmount",
284
- [return_amount, outcome_index],
284
+ "calcSellAmount", [return_amount, outcome_index], web3=web3
285
285
  )
286
286
  return calculated_shares
287
287
 
288
- def conditionalTokens(
289
- self,
290
- ) -> HexAddress:
291
- address: HexAddress = self.call(
292
- "conditionalTokens",
293
- )
288
+ def conditionalTokens(self, web3: Web3 | None = None) -> HexAddress:
289
+ address: HexAddress = self.call("conditionalTokens", web3=web3)
294
290
  return address
295
291
 
296
292
  def buy(
@@ -321,6 +317,7 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
321
317
  outcome_index: int,
322
318
  max_outcome_tokens_to_sell: OmenOutcomeToken,
323
319
  tx_params: t.Optional[TxParams] = None,
320
+ web3: Web3 | None = None,
324
321
  ) -> TxReceipt:
325
322
  return self.send(
326
323
  private_credentials=private_credentials,
@@ -331,6 +328,7 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
331
328
  max_outcome_tokens_to_sell,
332
329
  ],
333
330
  tx_params=tx_params,
331
+ web3=web3,
334
332
  )
335
333
 
336
334
  def addFunding(
@@ -338,6 +336,7 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
338
336
  private_credentials: PrivateCredentials,
339
337
  add_funding: Wei,
340
338
  tx_params: t.Optional[TxParams] = None,
339
+ web3: Web3 | None = None,
341
340
  ) -> TxReceipt:
342
341
  """
343
342
  Funding is added in Weis (xDai) and then converted to shares.
@@ -349,6 +348,7 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
349
348
  function_name="addFunding",
350
349
  function_params=[add_funding, distribution_hint],
351
350
  tx_params=tx_params,
351
+ web3=web3,
352
352
  )
353
353
 
354
354
  def removeFunding(
@@ -369,9 +369,9 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
369
369
  web3=web3,
370
370
  )
371
371
 
372
- def totalSupply(self) -> Wei:
372
+ def totalSupply(self, web3: Web3 | None = None) -> Wei:
373
373
  # This is the liquidity you seen on the Omen website (but in Wei).
374
- total_supply: Wei = self.call("totalSupply")
374
+ total_supply: Wei = self.call("totalSupply", web3=web3)
375
375
  return total_supply
376
376
 
377
377
 
@@ -412,6 +412,7 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
412
412
  initial_funds_wei: Wei,
413
413
  fee: float = OMEN_DEFAULT_MARKET_FEE,
414
414
  tx_params: t.Optional[TxParams] = None,
415
+ web3: Web3 | None = None,
415
416
  ) -> TxReceipt:
416
417
  fee_wei = xdai_to_wei(
417
418
  xdai_type(fee)
@@ -431,6 +432,7 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
431
432
  distributionHint=[],
432
433
  ),
433
434
  tx_params=tx_params,
435
+ web3=web3,
434
436
  )
435
437
 
436
438
 
@@ -498,6 +500,7 @@ class OmenRealitioContract(ContractOnGnosisChain):
498
500
  opening: datetime,
499
501
  nonce: int | None = None,
500
502
  tx_params: t.Optional[TxParams] = None,
503
+ web3: Web3 | None = None,
501
504
  ) -> HexBytes:
502
505
  """
503
506
  After the question is created, you can find it at https://reality.eth.link/app/#!/creator/{from_address}.
@@ -528,6 +531,7 @@ class OmenRealitioContract(ContractOnGnosisChain):
528
531
  ), # Two equal questions need to have different nonces.
529
532
  ),
530
533
  tx_params=tx_params,
534
+ web3=web3,
531
535
  )
532
536
  question_id = HexBytes(
533
537
  receipt_tx["logs"][0]["topics"][1]
@@ -542,6 +546,7 @@ class OmenRealitioContract(ContractOnGnosisChain):
542
546
  outcomes: list[str],
543
547
  bond: Wei,
544
548
  max_previous: Wei | None = None,
549
+ web3: Web3 | None = None,
545
550
  ) -> TxReceipt:
546
551
  if max_previous is None:
547
552
  # If not provided, defaults to 0, which means no checking,
@@ -563,6 +568,7 @@ class OmenRealitioContract(ContractOnGnosisChain):
563
568
  max_previous=max_previous,
564
569
  ),
565
570
  amount_wei=bond,
571
+ web3=web3,
566
572
  )
567
573
 
568
574
  def claimWinnings(
@@ -574,6 +580,7 @@ class OmenRealitioContract(ContractOnGnosisChain):
574
580
  bonds: list[Wei],
575
581
  answers: list[HexBytes],
576
582
  tx_params: t.Optional[TxParams] = None,
583
+ web3: Web3 | None = None,
577
584
  ) -> TxReceipt:
578
585
  return self.send(
579
586
  private_credentials=private_credentials,
@@ -586,17 +593,22 @@ class OmenRealitioContract(ContractOnGnosisChain):
586
593
  answers=answers,
587
594
  ),
588
595
  tx_params=tx_params,
596
+ web3=web3,
589
597
  )
590
598
 
591
- def balanceOf(self, from_address: ChecksumAddress) -> Wei:
592
- balance = wei_type(self.call("balanceOf", [from_address]))
599
+ def balanceOf(
600
+ self,
601
+ from_address: ChecksumAddress,
602
+ web3: Web3 | None = None,
603
+ ) -> Wei:
604
+ balance = wei_type(self.call("balanceOf", [from_address], web3=web3))
593
605
  return balance
594
606
 
595
607
  def withdraw(
596
608
  self,
597
609
  private_credentials: PrivateCredentials,
610
+ web3: Web3 | None = None,
598
611
  ) -> TxReceipt:
599
612
  return self.send(
600
- private_credentials=private_credentials,
601
- function_name="withdraw",
613
+ private_credentials=private_credentials, function_name="withdraw", web3=web3
602
614
  )
@@ -38,6 +38,7 @@ def claim_bonds_on_realitio_questions(
38
38
  private_credentials: PrivateCredentials,
39
39
  questions: list[RealityQuestion],
40
40
  auto_withdraw: bool,
41
+ web3: Web3 | None = None,
41
42
  ) -> list[HexBytes]:
42
43
  claimed_questions: list[HexBytes] = []
43
44
 
@@ -46,7 +47,7 @@ def claim_bonds_on_realitio_questions(
46
47
  f"[{idx+1} / {len(questions)}] Claiming bond for {question.questionId=} {question.url=}"
47
48
  )
48
49
  claim_bonds_on_realitio_question(
49
- private_credentials, question, auto_withdraw=auto_withdraw
50
+ private_credentials, question, auto_withdraw=auto_withdraw, web3=web3
50
51
  )
51
52
  claimed_questions.append(question.questionId)
52
53
 
@@ -57,6 +58,7 @@ def claim_bonds_on_realitio_question(
57
58
  private_credentials: PrivateCredentials,
58
59
  question: RealityQuestion,
59
60
  auto_withdraw: bool,
61
+ web3: Web3 | None = None,
60
62
  ) -> None:
61
63
  public_key = private_credentials.public_key
62
64
  realitio_contract = OmenRealitioContract()
@@ -109,18 +111,20 @@ def claim_bonds_on_realitio_question(
109
111
  addresses=addresses,
110
112
  bonds=bonds,
111
113
  answers=answers,
114
+ web3=web3,
112
115
  )
113
116
 
114
- current_balance = realitio_contract.balanceOf(public_key)
117
+ current_balance = realitio_contract.balanceOf(public_key, web3=web3)
115
118
  # Keeping balance on Realitio is not useful, so it's recommended to just withdraw it.
116
119
  if current_balance > 0 and auto_withdraw:
117
120
  logger.info(f"Withdrawing remaining balance {current_balance=}")
118
- realitio_contract.withdraw(private_credentials)
121
+ realitio_contract.withdraw(private_credentials, web3=web3)
119
122
 
120
123
 
121
124
  def finalize_markets(
122
125
  private_credentials: PrivateCredentials,
123
126
  markets_with_resolutions: list[tuple[OmenMarket, Resolution | None]],
127
+ web3: Web3 | None = None,
124
128
  ) -> list[HexAddress]:
125
129
  finalized_markets: list[HexAddress] = []
126
130
 
@@ -139,6 +143,7 @@ def finalize_markets(
139
143
  market,
140
144
  resolution,
141
145
  OMEN_DEFAULT_REALITIO_BOND_VALUE,
146
+ web3=web3,
142
147
  )
143
148
  finalized_markets.append(market.id)
144
149
  logger.info(f"Finalized {market.url=}")
@@ -152,6 +157,7 @@ def finalize_markets(
152
157
  def resolve_markets(
153
158
  private_credentials: PrivateCredentials,
154
159
  markets: list[OmenMarket],
160
+ web3: Web3 | None = None,
155
161
  ) -> list[HexAddress]:
156
162
  resolved_markets: list[HexAddress] = []
157
163
 
@@ -159,7 +165,7 @@ def resolve_markets(
159
165
  logger.info(
160
166
  f"[{idx+1} / {len(markets)}] Resolving {market.url=} {market.question_title=}"
161
167
  )
162
- omen_resolve_market_tx(private_credentials, market)
168
+ omen_resolve_market_tx(private_credentials, market, web3=web3)
163
169
  resolved_markets.append(market.id)
164
170
 
165
171
  return resolved_markets
@@ -170,6 +176,7 @@ def omen_submit_answer_market_tx(
170
176
  market: OmenMarket,
171
177
  resolution: Resolution,
172
178
  bond: xDai,
179
+ web3: Web3 | None = None,
173
180
  ) -> None:
174
181
  """
175
182
  After the answer is submitted, there is 24h waiting period where the answer can be challenged by others.
@@ -182,12 +189,14 @@ def omen_submit_answer_market_tx(
182
189
  answer=resolution.value,
183
190
  outcomes=market.question.outcomes,
184
191
  bond=xdai_to_wei(bond),
192
+ web3=web3,
185
193
  )
186
194
 
187
195
 
188
196
  def omen_resolve_market_tx(
189
197
  private_credentials: PrivateCredentials,
190
198
  market: OmenMarket,
199
+ web3: Web3 | None = None,
191
200
  ) -> None:
192
201
  """
193
202
  Market can be resolved 24h after last answer was submitted via `omen_submit_answer_market_tx`.
@@ -199,6 +208,7 @@ def omen_resolve_market_tx(
199
208
  template_id=market.question.templateId,
200
209
  question_raw=market.question.question_raw,
201
210
  n_outcomes=market.question.n_outcomes,
211
+ web3=web3,
202
212
  )
203
213
 
204
214
 
@@ -0,0 +1,26 @@
1
+ from functools import cached_property
2
+
3
+ from langfuse.callback import CallbackHandler
4
+ from pydantic import BaseModel, computed_field
5
+
6
+ from prediction_market_agent_tooling.config import APIKeys
7
+ from prediction_market_agent_tooling.tools.utils import utcnow
8
+
9
+
10
+ class LangfuseWrapper(BaseModel):
11
+ agent_name: str
12
+
13
+ @computed_field # type: ignore[misc] # Mypy issue: https://github.com/python/mypy/issues/14461
14
+ @cached_property
15
+ def session_id(self) -> str:
16
+ return f"{self.agent_name} - {utcnow()}"
17
+
18
+ def get_langfuse_handler(self) -> CallbackHandler:
19
+ keys = APIKeys()
20
+ langfuse_handler = CallbackHandler(
21
+ secret_key=keys.langfuse_secret_key.get_secret_value(),
22
+ public_key=keys.langfuse_public_key.get_secret_value(),
23
+ host=keys.langfuse_host,
24
+ session_id=self.session_id,
25
+ )
26
+ return langfuse_handler
@@ -258,27 +258,31 @@ def monitor_brier_score(resolved_markets: t.Sequence[AgentMarket]) -> None:
258
258
  - the overall brier score
259
259
  - the brier score for the last 30 markets
260
260
  """
261
- st.subheader("Brier Score (0-1, lower is better)")
261
+ st.subheader("Brier Score (0-2, lower is better)")
262
262
 
263
263
  # We need to use `get_last_trade_p_yes` instead of `current_p_yes` because, for resolved markets, the probabilities can be fixed to 0 and 1 (for example, on Omen).
264
264
  # And for the brier score, we need the true market prediction, not its resolution after the outcome is known.
265
265
  # If no trades were made, take it as 0.5 because the platform didn't provide any valuable information.
266
- created_time_and_squared_errors = par_map(
266
+ created_time_and_squared_errors_summed_across_outcomes = par_map(
267
267
  list(resolved_markets),
268
268
  lambda m: (
269
269
  m.created_time,
270
270
  (
271
271
  (p_yes - m.boolean_outcome) ** 2
272
+ + ((1 - p_yes) - (1 - m.boolean_outcome)) ** 2
272
273
  if (p_yes := m.get_last_trade_p_yes()) is not None
273
274
  else None
274
275
  ),
275
276
  ),
276
277
  )
277
- created_time_and_squared_errors_with_trades = [
278
- x for x in created_time_and_squared_errors if x[1] is not None
278
+ created_time_and_squared_errors_summed_across_outcomes_with_trades = [
279
+ x
280
+ for x in created_time_and_squared_errors_summed_across_outcomes
281
+ if x[1] is not None
279
282
  ]
280
283
  df = pd.DataFrame(
281
- created_time_and_squared_errors_with_trades, columns=["Date", "Squared Error"]
284
+ created_time_and_squared_errors_summed_across_outcomes_with_trades,
285
+ columns=["Date", "Squared Error"],
282
286
  ).sort_values(by="Date")
283
287
 
284
288
  # Compute rolling mean squared error for last 30 markets
@@ -1,10 +1,11 @@
1
1
  from pydantic import BaseModel
2
+ from web3 import Web3
3
+ from web3.types import Wei
2
4
 
3
5
  from prediction_market_agent_tooling.gtypes import ChecksumAddress, xDai
4
6
  from prediction_market_agent_tooling.markets.omen.omen_contracts import (
5
7
  WrappedxDaiContract,
6
8
  )
7
- from prediction_market_agent_tooling.tools.gnosis_rpc import get_balance
8
9
  from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai
9
10
 
10
11
 
@@ -13,7 +14,10 @@ class Balances(BaseModel):
13
14
  wxdai: xDai
14
15
 
15
16
 
16
- def get_balances(address: ChecksumAddress) -> Balances:
17
- xdai = wei_to_xdai(get_balance(address))
18
- wxdai = wei_to_xdai(WrappedxDaiContract().balanceOf(address))
17
+ def get_balances(address: ChecksumAddress, web3: Web3 | None) -> Balances:
18
+ if not web3:
19
+ web3 = WrappedxDaiContract().get_web3()
20
+ xdai_balance = Wei(web3.eth.get_balance(address))
21
+ xdai = wei_to_xdai(xdai_balance)
22
+ wxdai = wei_to_xdai(WrappedxDaiContract().balanceOf(address, web3=web3))
19
23
  return Balances(xdai=xdai, wxdai=wxdai)
@@ -190,6 +190,23 @@ class ContractERC20BaseClass(ContractBaseClass):
190
190
  web3=web3,
191
191
  )
192
192
 
193
+ def transferFrom(
194
+ self,
195
+ private_credentials: PrivateCredentials,
196
+ sender: ChecksumAddress,
197
+ recipient: ChecksumAddress,
198
+ amount_wei: Wei,
199
+ tx_params: t.Optional[TxParams] = None,
200
+ web3: Web3 | None = None,
201
+ ) -> TxReceipt:
202
+ return self.send(
203
+ private_credentials=private_credentials,
204
+ function_name="transferFrom",
205
+ function_params=[sender, recipient, amount_wei],
206
+ tx_params=tx_params,
207
+ web3=web3,
208
+ )
209
+
193
210
  def withdraw(
194
211
  self,
195
212
  private_credentials: PrivateCredentials,
@@ -0,0 +1,6 @@
1
+ import os
2
+
3
+ from prediction_market_agent_tooling.gtypes import ChainID
4
+
5
+ GNOSIS_NETWORK_ID = ChainID(100) # xDai network.
6
+ GNOSIS_RPC_URL = os.getenv("GNOSIS_RPC_URL", "https://gnosis-rpc.publicnode.com")
@@ -116,8 +116,20 @@ def prepare_tx(
116
116
  function_params: Optional[list[Any] | dict[str, Any]] = None,
117
117
  tx_params: Optional[TxParams] = None,
118
118
  ) -> TxParams:
119
+ tx_params_new = _prepare_tx_params(web3, from_address, tx_params)
119
120
  contract = web3.eth.contract(address=contract_address, abi=contract_abi)
120
121
 
122
+ # Build the transaction.
123
+ function_call = contract.functions[function_name](*parse_function_params(function_params)) # type: ignore # TODO: Fix Mypy, as this works just OK.
124
+ tx_params_new = function_call.build_transaction(tx_params_new)
125
+ return tx_params_new
126
+
127
+
128
+ def _prepare_tx_params(
129
+ web3: Web3,
130
+ from_address: ChecksumAddress | None,
131
+ tx_params: Optional[TxParams] = None,
132
+ ) -> TxParams:
121
133
  # Fill in required defaults, if not provided.
122
134
  tx_params_new = TxParams()
123
135
  if tx_params:
@@ -135,11 +147,6 @@ def prepare_tx(
135
147
  from_checksummed = Web3.to_checksum_address(tx_params_new["from"])
136
148
  tx_params_new["nonce"] = web3.eth.get_transaction_count(from_checksummed)
137
149
 
138
- # Build the transaction.
139
- function_call = contract.functions[function_name](*parse_function_params(function_params)) # type: ignore # TODO: Fix Mypy, as this works just OK.
140
- tx_params_new = function_call.build_transaction(tx_params_new)
141
- gas = web3.eth.estimate_gas(tx_params_new)
142
- tx_params_new["gas"] = gas
143
150
  return tx_params_new
144
151
 
145
152
 
@@ -176,16 +183,10 @@ def send_function_on_contract_tx(
176
183
  function_params=function_params,
177
184
  tx_params=tx_params,
178
185
  )
179
- # Sign with the private key.
180
- signed_tx = web3.eth.account.sign_transaction(
181
- tx_params, private_key=from_private_key.get_secret_value()
186
+
187
+ receipt_tx = sign_send_and_get_receipt_tx(
188
+ web3, tx_params, from_private_key, timeout
182
189
  )
183
- # Send the signed transaction.
184
- send_tx = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
185
- # And wait for the receipt.
186
- receipt_tx = web3.eth.wait_for_transaction_receipt(send_tx, timeout=timeout)
187
- # Verify it didn't fail.
188
- check_tx_receipt(receipt_tx)
189
190
  return receipt_tx
190
191
 
191
192
 
@@ -237,3 +238,52 @@ def send_function_on_contract_tx_using_safe(
237
238
  receipt_tx = web3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
238
239
  check_tx_receipt(receipt_tx)
239
240
  return receipt_tx
241
+
242
+
243
+ def sign_send_and_get_receipt_tx(
244
+ web3: Web3,
245
+ tx_params_new: TxParams,
246
+ from_private_key: PrivateKey,
247
+ timeout: int = 180,
248
+ ) -> TxReceipt:
249
+ # Sign with the private key.
250
+ signed_tx = web3.eth.account.sign_transaction(
251
+ tx_params_new, private_key=from_private_key.get_secret_value()
252
+ )
253
+ # Send the signed transaction.
254
+ send_tx = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
255
+ # And wait for the receipt.
256
+ receipt_tx = web3.eth.wait_for_transaction_receipt(send_tx, timeout=timeout)
257
+ # Verify it didn't fail.
258
+ check_tx_receipt(receipt_tx)
259
+ return receipt_tx
260
+
261
+
262
+ def send_xdai_to(
263
+ web3: Web3,
264
+ from_private_key: PrivateKey,
265
+ to_address: ChecksumAddress,
266
+ value: Wei,
267
+ tx_params: Optional[TxParams] = None,
268
+ timeout: int = 180,
269
+ ) -> TxReceipt:
270
+ from_address = private_key_to_public_key(from_private_key)
271
+
272
+ tx_params_new: TxParams = {"value": value, "to": to_address}
273
+ if tx_params:
274
+ tx_params_new.update(tx_params)
275
+ tx_params_new = _prepare_tx_params(web3, from_address, tx_params_new)
276
+
277
+ # We need gas and gasPrice here (and not elsewhere) because we are not calling
278
+ # contract.functions.myFunction().build_transaction, which autofills some params
279
+ # with defaults, incl. gas and gasPrice.
280
+ gas = web3.eth.estimate_gas(tx_params_new)
281
+ tx_params_new["gas"] = int(
282
+ gas * 1.5
283
+ ) # We conservatively overestimate gas here, knowing it will be returned if unused
284
+ tx_params_new["gasPrice"] = web3.eth.gas_price
285
+
286
+ receipt_tx = sign_send_and_get_receipt_tx(
287
+ web3, tx_params_new, from_private_key, timeout
288
+ )
289
+ return receipt_tx
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "prediction-market-agent-tooling"
3
- version = "0.17.0"
3
+ version = "0.19.0"
4
4
  description = "Tools to benchmark, deploy and monitor prediction market agents."
5
5
  authors = ["Gnosis"]
6
6
  readme = "README.md"
@@ -41,6 +41,7 @@ eth-account = "^0.8.0"
41
41
  prompt-toolkit = "^3.0.43"
42
42
  safe-cli = "^1.0.0"
43
43
  mech-client = "^0.2.13"
44
+ langfuse = "^2.27.1"
44
45
 
45
46
  [tool.poetry.extras]
46
47
  langchain = ["langchain", "langchain-openai"]
@@ -1,24 +0,0 @@
1
- import os
2
-
3
- import requests
4
-
5
- from prediction_market_agent_tooling.gtypes import ChainID, HexAddress, HexBytes, Wei
6
-
7
- GNOSIS_NETWORK_ID = ChainID(100) # xDai network.
8
- GNOSIS_RPC_URL = os.getenv("GNOSIS_RPC_URL", "https://gnosis-rpc.publicnode.com")
9
-
10
-
11
- def get_balance(address: HexAddress) -> Wei:
12
- response = requests.post(
13
- GNOSIS_RPC_URL,
14
- json={
15
- "jsonrpc": "2.0",
16
- "method": "eth_getBalance",
17
- "params": [address, "latest"],
18
- "id": 1,
19
- },
20
- headers={"content-type": "application/json"},
21
- ).json()
22
- balance_bytes = HexBytes(response["result"])
23
- balance = Wei(balance_bytes.as_int())
24
- return balance