prediction-market-agent-tooling 0.49.0__py3-none-any.whl → 0.49.2__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 (30) hide show
  1. prediction_market_agent_tooling/abis/debuggingcontract.abi.json +29 -0
  2. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +171 -0
  3. prediction_market_agent_tooling/benchmark/benchmark.py +0 -93
  4. prediction_market_agent_tooling/config.py +16 -0
  5. prediction_market_agent_tooling/deploy/agent.py +62 -7
  6. prediction_market_agent_tooling/deploy/betting_strategy.py +83 -3
  7. prediction_market_agent_tooling/markets/agent_market.py +5 -3
  8. prediction_market_agent_tooling/markets/data_models.py +1 -1
  9. prediction_market_agent_tooling/markets/metaculus/metaculus.py +1 -1
  10. prediction_market_agent_tooling/markets/omen/data_models.py +240 -5
  11. prediction_market_agent_tooling/markets/omen/omen.py +44 -25
  12. prediction_market_agent_tooling/markets/omen/omen_contracts.py +180 -33
  13. prediction_market_agent_tooling/markets/omen/omen_resolving.py +1 -1
  14. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +45 -0
  15. prediction_market_agent_tooling/markets/polymarket/api.py +1 -1
  16. prediction_market_agent_tooling/monitor/monitor.py +3 -3
  17. prediction_market_agent_tooling/monitor/monitor_app.py +2 -2
  18. prediction_market_agent_tooling/tools/contract.py +50 -1
  19. prediction_market_agent_tooling/tools/httpx_cached_client.py +11 -0
  20. prediction_market_agent_tooling/tools/ipfs/ipfs_handler.py +33 -0
  21. prediction_market_agent_tooling/tools/is_predictable.py +1 -1
  22. prediction_market_agent_tooling/tools/langfuse_client_utils.py +23 -8
  23. prediction_market_agent_tooling/tools/tavily_storage/tavily_models.py +1 -1
  24. prediction_market_agent_tooling/tools/utils.py +28 -4
  25. prediction_market_agent_tooling/tools/web3_utils.py +7 -0
  26. {prediction_market_agent_tooling-0.49.0.dist-info → prediction_market_agent_tooling-0.49.2.dist-info}/METADATA +3 -1
  27. {prediction_market_agent_tooling-0.49.0.dist-info → prediction_market_agent_tooling-0.49.2.dist-info}/RECORD +30 -26
  28. {prediction_market_agent_tooling-0.49.0.dist-info → prediction_market_agent_tooling-0.49.2.dist-info}/LICENSE +0 -0
  29. {prediction_market_agent_tooling-0.49.0.dist-info → prediction_market_agent_tooling-0.49.2.dist-info}/WHEEL +0 -0
  30. {prediction_market_agent_tooling-0.49.0.dist-info → prediction_market_agent_tooling-0.49.2.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
5
5
  from enum import Enum
6
6
 
7
7
  from web3 import Web3
8
+ from web3.constants import HASH_ZERO
8
9
 
9
10
  from prediction_market_agent_tooling.config import APIKeys
10
11
  from prediction_market_agent_tooling.gtypes import (
@@ -20,10 +21,15 @@ from prediction_market_agent_tooling.gtypes import (
20
21
  Wei,
21
22
  int_to_hexbytes,
22
23
  wei_type,
23
- xdai_type,
24
24
  )
25
25
  from prediction_market_agent_tooling.markets.omen.data_models import (
26
26
  INVALID_ANSWER_HEX_BYTES,
27
+ ConditionPreparationEvent,
28
+ ContractPrediction,
29
+ FPMMFundingAddedEvent,
30
+ OmenFixedProductMarketMakerCreationEvent,
31
+ RealitioLogNewQuestionEvent,
32
+ format_realitio_question,
27
33
  )
28
34
  from prediction_market_agent_tooling.tools.contract import (
29
35
  ContractDepositableWrapperERC20OnGnosisChain,
@@ -38,7 +44,6 @@ from prediction_market_agent_tooling.tools.web3_utils import (
38
44
  ZERO_BYTES,
39
45
  byte32_to_ipfscidv0,
40
46
  ipfscidv0_to_byte32,
41
- xdai_to_wei,
42
47
  )
43
48
 
44
49
 
@@ -84,6 +89,10 @@ class OmenOracleContract(ContractOnGnosisChain):
84
89
  )
85
90
 
86
91
 
92
+ def build_parent_collection_id() -> HexStr:
93
+ return HASH_ZERO # Taken from Olas
94
+
95
+
87
96
  class OmenConditionalTokenContract(ContractOnGnosisChain):
88
97
  # Contract ABI taken from https://gnosisscan.io/address/0xCeAfDD6bc0bEF976fdCd1112955828E00543c0Ce#code.
89
98
  abi: ABI = abi_field_validator(
@@ -177,8 +186,8 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
177
186
  api_keys: APIKeys,
178
187
  collateral_token_address: HexAddress,
179
188
  condition_id: HexBytes,
180
- parent_collection_id: HexStr,
181
189
  index_sets: t.List[int],
190
+ parent_collection_id: HexStr = build_parent_collection_id(),
182
191
  web3: Web3 | None = None,
183
192
  ) -> TxReceipt:
184
193
  return self.send(
@@ -247,8 +256,8 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
247
256
  outcomes_slot_count: int,
248
257
  tx_params: t.Optional[TxParams] = None,
249
258
  web3: Web3 | None = None,
250
- ) -> TxReceipt:
251
- return self.send(
259
+ ) -> ConditionPreparationEvent:
260
+ receipt_tx = self.send(
252
261
  api_keys=api_keys,
253
262
  function_name="prepareCondition",
254
263
  function_params=[
@@ -260,6 +269,15 @@ class OmenConditionalTokenContract(ContractOnGnosisChain):
260
269
  web3=web3,
261
270
  )
262
271
 
272
+ event_logs = (
273
+ self.get_web3_contract(web3=web3)
274
+ .events.ConditionPreparation()
275
+ .process_receipt(receipt_tx)
276
+ )
277
+ cond_event = ConditionPreparationEvent(**event_logs[0]["args"])
278
+
279
+ return cond_event
280
+
263
281
 
264
282
  class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
265
283
  # File content taken from https://github.com/protofire/omen-exchange/blob/master/app/src/abi/marketMaker.json.
@@ -412,7 +430,8 @@ class sDaiContract(ContractERC4626OnGnosisChain):
412
430
  )
413
431
 
414
432
 
415
- OMEN_DEFAULT_MARKET_FEE = 0.02 # 2% fee from the buying shares amount.
433
+ OMEN_DEFAULT_MARKET_FEE_PERC = 0.02 # 2% fee from the buying shares amount.
434
+ REALITY_DEFAULT_FINALIZATION_TIMEOUT = timedelta(days=3)
416
435
 
417
436
 
418
437
  class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
@@ -433,14 +452,15 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
433
452
  condition_id: HexBytes,
434
453
  initial_funds_wei: Wei,
435
454
  collateral_token_address: ChecksumAddress,
436
- fee: float = OMEN_DEFAULT_MARKET_FEE,
455
+ fee: Wei, # This is actually fee in %, 'where 100% == 1 xDai'.
456
+ distribution_hint: list[OmenOutcomeToken] | None = None,
437
457
  tx_params: t.Optional[TxParams] = None,
438
458
  web3: Web3 | None = None,
439
- ) -> TxReceipt:
440
- fee_wei = xdai_to_wei(
441
- xdai_type(fee)
442
- ) # We need to convert this to the wei units, but in reality it's % fee as stated in the `OMEN_DEFAULT_MARKET_FEE` variable.
443
- return self.send(
459
+ ) -> tuple[
460
+ OmenFixedProductMarketMakerCreationEvent, FPMMFundingAddedEvent, TxReceipt
461
+ ]:
462
+ web3 = web3 or self.get_web3()
463
+ receipt_tx = self.send(
444
464
  api_keys=api_keys,
445
465
  function_name="create2FixedProductMarketMaker",
446
466
  function_params=dict(
@@ -450,19 +470,41 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
450
470
  conditionalTokens=OmenConditionalTokenContract().address,
451
471
  collateralToken=collateral_token_address,
452
472
  conditionIds=[condition_id],
453
- fee=fee_wei,
473
+ fee=fee,
454
474
  initialFunds=initial_funds_wei,
455
- distributionHint=[],
475
+ distributionHint=distribution_hint or [],
456
476
  ),
457
477
  tx_params=tx_params,
458
478
  web3=web3,
459
479
  )
460
480
 
481
+ market_event_logs = (
482
+ self.get_web3_contract(web3=web3)
483
+ .events.FixedProductMarketMakerCreation()
484
+ .process_receipt(receipt_tx)
485
+ )
486
+ market_event = OmenFixedProductMarketMakerCreationEvent(
487
+ **market_event_logs[0]["args"]
488
+ )
489
+ funding_event_logs = (
490
+ self.get_web3_contract(web3=web3)
491
+ .events.FPMMFundingAdded()
492
+ .process_receipt(receipt_tx)
493
+ )
494
+ funding_event = FPMMFundingAddedEvent(**funding_event_logs[0]["args"])
495
+
496
+ return market_event, funding_event, receipt_tx
497
+
461
498
 
462
499
  class Arbitrator(str, Enum):
463
- KLEROS = "kleros"
500
+ KLEROS_511_JURORS_WITHOUT_APPEAL = "kleros_511_jurors_without_appeal"
501
+ KLEROS_31_JURORS_WITH_APPEAL = "kleros_31_jurors_with_appeal"
464
502
  DXDAO = "dxdao"
465
503
 
504
+ @property
505
+ def is_kleros(self) -> bool:
506
+ return self.value.startswith("kleros")
507
+
466
508
 
467
509
  class OmenDxDaoContract(ContractOnGnosisChain):
468
510
  # Contract ABI taken from https://gnosisscan.io/address/0xFe14059344b74043Af518d12931600C0f52dF7c5#code.
@@ -485,9 +527,22 @@ class OmenKlerosContract(ContractOnGnosisChain):
485
527
  "../../abis/omen_kleros.abi.json",
486
528
  )
487
529
  )
488
- address: ChecksumAddress = Web3.to_checksum_address(
489
- "0xe40DD83a262da3f56976038F1554Fe541Fa75ecd"
490
- )
530
+
531
+ @staticmethod
532
+ def from_arbitrator(arbitrator: "Arbitrator") -> "OmenKlerosContract":
533
+ """
534
+ See https://docs.kleros.io/developer/deployment-addresses for all available addresses.
535
+ """
536
+ if arbitrator == Arbitrator.KLEROS_511_JURORS_WITHOUT_APPEAL:
537
+ address = "0xe40DD83a262da3f56976038F1554Fe541Fa75ecd"
538
+
539
+ elif arbitrator == Arbitrator.KLEROS_31_JURORS_WITH_APPEAL:
540
+ address = "0x29f39de98d750eb77b5fafb31b2837f079fce222"
541
+
542
+ else:
543
+ raise ValueError(f"Unsupported arbitrator: {arbitrator=}")
544
+
545
+ return OmenKlerosContract(address=Web3.to_checksum_address(address))
491
546
 
492
547
 
493
548
  class OmenRealitioContract(ContractOnGnosisChain):
@@ -506,8 +561,8 @@ class OmenRealitioContract(ContractOnGnosisChain):
506
561
  def get_arbitrator_contract(
507
562
  arbitrator: Arbitrator,
508
563
  ) -> ContractOnGnosisChain:
509
- if arbitrator == Arbitrator.KLEROS:
510
- return OmenKlerosContract()
564
+ if arbitrator.is_kleros:
565
+ return OmenKlerosContract.from_arbitrator(arbitrator)
511
566
  if arbitrator == Arbitrator.DXDAO:
512
567
  return OmenDxDaoContract()
513
568
  raise ValueError(f"Unknown arbitrator: {arbitrator}")
@@ -521,25 +576,25 @@ class OmenRealitioContract(ContractOnGnosisChain):
521
576
  language: str,
522
577
  arbitrator: Arbitrator,
523
578
  opening: datetime,
524
- timeout: timedelta = timedelta(days=1),
579
+ timeout: timedelta,
525
580
  nonce: int | None = None,
526
581
  tx_params: t.Optional[TxParams] = None,
527
582
  web3: Web3 | None = None,
528
- ) -> HexBytes:
583
+ ) -> RealitioLogNewQuestionEvent:
529
584
  """
530
585
  After the question is created, you can find it at https://reality.eth.link/app/#!/creator/{from_address}.
531
586
  """
587
+ web3 = web3 or self.get_web3()
532
588
  arbitrator_contract_address = self.get_arbitrator_contract(arbitrator).address
533
589
  # See https://realitio.github.io/docs/html/contracts.html#templates
534
590
  # for possible template ids and how to format the question.
535
591
  template_id = 2
536
- realitio_question = "␟".join(
537
- [
538
- question,
539
- ",".join(f'"{o}"' for o in outcomes),
540
- category,
541
- language,
542
- ]
592
+ realitio_question = format_realitio_question(
593
+ question=question,
594
+ outcomes=outcomes,
595
+ category=category,
596
+ language=language,
597
+ template_id=template_id,
543
598
  )
544
599
  receipt_tx = self.send(
545
600
  api_keys=api_keys,
@@ -557,10 +612,15 @@ class OmenRealitioContract(ContractOnGnosisChain):
557
612
  tx_params=tx_params,
558
613
  web3=web3,
559
614
  )
560
- question_id = HexBytes(
561
- receipt_tx["logs"][0]["topics"][1]
562
- ) # The question id is available in the first emitted log, in the second topic.
563
- return question_id
615
+
616
+ event_logs = (
617
+ self.get_web3_contract(web3=web3)
618
+ .events.LogNewQuestion()
619
+ .process_receipt(receipt_tx)
620
+ )
621
+ question_event = RealitioLogNewQuestionEvent(**event_logs[0]["args"])
622
+
623
+ return question_event
564
624
 
565
625
  def submitAnswer(
566
626
  self,
@@ -670,6 +730,93 @@ class OmenRealitioContract(ContractOnGnosisChain):
670
730
  ) -> TxReceipt:
671
731
  return self.send(api_keys=api_keys, function_name="withdraw", web3=web3)
672
732
 
733
+ def getOpeningTS(
734
+ self,
735
+ question_id: HexBytes,
736
+ web3: Web3 | None = None,
737
+ ) -> int:
738
+ ts: int = self.call(
739
+ function_name="getOpeningTS",
740
+ function_params=[question_id],
741
+ web3=web3,
742
+ )
743
+ return ts
744
+
745
+ def getFinalizeTS(
746
+ self,
747
+ question_id: HexBytes,
748
+ web3: Web3 | None = None,
749
+ ) -> int:
750
+ ts: int = self.call(
751
+ function_name="getFinalizeTS",
752
+ function_params=[question_id],
753
+ web3=web3,
754
+ )
755
+ return ts
756
+
757
+ def isFinalized(
758
+ self,
759
+ question_id: HexBytes,
760
+ web3: Web3 | None = None,
761
+ ) -> bool:
762
+ is_finalized: bool = self.call(
763
+ function_name="isFinalized",
764
+ function_params=[question_id],
765
+ web3=web3,
766
+ )
767
+ return is_finalized
768
+
769
+ def isPendingArbitration(
770
+ self,
771
+ question_id: HexBytes,
772
+ web3: Web3 | None = None,
773
+ ) -> bool:
774
+ is_pending_arbitration: bool = self.call(
775
+ function_name="isPendingArbitration",
776
+ function_params=[question_id],
777
+ web3=web3,
778
+ )
779
+ return is_pending_arbitration
780
+
781
+
782
+ class OmenAgentResultMappingContract(ContractOnGnosisChain):
783
+ # Contract ABI taken from built https://github.com/gnosis/labs-contracts.
784
+
785
+ abi: ABI = abi_field_validator(
786
+ os.path.join(
787
+ os.path.dirname(os.path.realpath(__file__)),
788
+ "../../abis/omen_agentresultmapping.abi.json",
789
+ )
790
+ )
791
+
792
+ address: ChecksumAddress = Web3.to_checksum_address(
793
+ "0x260E1077dEA98e738324A6cEfB0EE9A272eD471a"
794
+ )
795
+
796
+ def get_predictions(
797
+ self,
798
+ market_address: ChecksumAddress,
799
+ web3: Web3 | None = None,
800
+ ) -> list[ContractPrediction]:
801
+ prediction_tuples = self.call(
802
+ "getPredictions", function_params=[market_address], web3=web3
803
+ )
804
+ return [ContractPrediction.from_tuple(p) for p in prediction_tuples]
805
+
806
+ def add_prediction(
807
+ self,
808
+ api_keys: APIKeys,
809
+ market_address: ChecksumAddress,
810
+ prediction: ContractPrediction,
811
+ web3: Web3 | None = None,
812
+ ) -> TxReceipt:
813
+ return self.send(
814
+ api_keys=api_keys,
815
+ function_name="addPrediction",
816
+ function_params=[market_address, prediction.model_dump(by_alias=True)],
817
+ web3=web3,
818
+ )
819
+
673
820
 
674
821
  class OmenThumbnailMapping(ContractOnGnosisChain):
675
822
  # Contract ABI taken from built https://github.com/gnosis/labs-contracts.
@@ -240,7 +240,7 @@ def omen_resolve_market_tx(
240
240
  web3: Web3 | None = None,
241
241
  ) -> None:
242
242
  """
243
- Market can be resolved 24h after last answer was submitted via `omen_submit_answer_market_tx`.
243
+ Market can be resolved after the answer if finalized on Reality.
244
244
  """
245
245
  oracle_contract = OmenOracleContract()
246
246
  oracle_contract.resolve(
@@ -20,6 +20,7 @@ from prediction_market_agent_tooling.loggers import logger
20
20
  from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
21
21
  from prediction_market_agent_tooling.markets.omen.data_models import (
22
22
  OMEN_BINARY_MARKET_OUTCOMES,
23
+ ContractPrediction,
23
24
  OmenBet,
24
25
  OmenMarket,
25
26
  OmenPosition,
@@ -60,6 +61,8 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
60
61
 
61
62
  OMEN_IMAGE_MAPPING_GRAPH_URL = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/EWN14ciGK53PpUiKSm7kMWQ6G4iz3tDrRLyZ1iXMQEdu"
62
63
 
64
+ OMEN_AGENT_RESULT_MAPPING_GRAPH_URL = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/GoE3UFyc8Gg9xzv92oinonyhRCphpGu62qB2Eh2XvJ8F"
65
+
63
66
  INVALID_ANSWER = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
64
67
 
65
68
  def __init__(self) -> None:
@@ -96,6 +99,12 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
96
99
  )
97
100
  )
98
101
 
102
+ self.omen_agent_result_mapping_subgraph = self.sg.load_subgraph(
103
+ self.OMEN_AGENT_RESULT_MAPPING_GRAPH_URL.format(
104
+ graph_api_key=keys.graph_api_key.get_secret_value()
105
+ )
106
+ )
107
+
99
108
  def _get_fields_for_bets(self, bets_field: FieldPath) -> list[FieldPath]:
100
109
  markets = bets_field.fpmm
101
110
  fields_for_markets = self._get_fields_for_markets(markets)
@@ -310,6 +319,16 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
310
319
  sort_by_field = (
311
320
  self.trades_subgraph.FixedProductMarketMaker.openingTimestamp
312
321
  )
322
+ case SortBy.HIGHEST_LIQUIDITY:
323
+ sort_direction = "desc"
324
+ sort_by_field = (
325
+ self.trades_subgraph.FixedProductMarketMaker.liquidityMeasure
326
+ )
327
+ case SortBy.LOWEST_LIQUIDITY:
328
+ sort_direction = "asc"
329
+ sort_by_field = (
330
+ self.trades_subgraph.FixedProductMarketMaker.liquidityMeasure
331
+ )
313
332
  case SortBy.NONE:
314
333
  sort_direction = None
315
334
  sort_by_field = None
@@ -797,3 +816,29 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
797
816
  if image_url is not None
798
817
  else None
799
818
  )
819
+
820
+ def get_agent_results_for_market(
821
+ self, market_id: HexAddress | None = None
822
+ ) -> list[ContractPrediction]:
823
+ where_stms = {}
824
+ if market_id:
825
+ where_stms["marketAddress"] = market_id.lower()
826
+
827
+ prediction_added = (
828
+ self.omen_agent_result_mapping_subgraph.Query.predictionAddeds(
829
+ where=where_stms,
830
+ orderBy="blockNumber",
831
+ orderDirection="asc",
832
+ )
833
+ )
834
+ fields = [
835
+ prediction_added.publisherAddress,
836
+ prediction_added.ipfsHash,
837
+ prediction_added.txHashes,
838
+ prediction_added.estimatedProbabilityBps,
839
+ ]
840
+ result = self.sg.query_json(fields)
841
+ items = self._parse_items_from_json(result)
842
+ if not items:
843
+ return []
844
+ return [ContractPrediction.model_validate(i) for i in items]
@@ -2,8 +2,8 @@ import typing as t
2
2
 
3
3
  import requests
4
4
  import tenacity
5
- from loguru import logger
6
5
 
6
+ from prediction_market_agent_tooling.loggers import logger
7
7
  from prediction_market_agent_tooling.markets.polymarket.data_models import (
8
8
  POLYMARKET_FALSE_OUTCOME,
9
9
  POLYMARKET_TRUE_OUTCOME,
@@ -27,8 +27,8 @@ from prediction_market_agent_tooling.markets.data_models import Resolution, Reso
27
27
  from prediction_market_agent_tooling.tools.parallelism import par_map
28
28
  from prediction_market_agent_tooling.tools.utils import (
29
29
  DatetimeWithTimezone,
30
- add_utc_timezone_validator,
31
30
  check_not_none,
31
+ convert_to_utc_datetime,
32
32
  should_not_happen,
33
33
  )
34
34
 
@@ -49,10 +49,10 @@ class DeployedAgent(BaseModel):
49
49
  raw_env_vars: dict[str, str] | None = None
50
50
 
51
51
  _add_timezone_validator_start_time = field_validator("start_time")(
52
- add_utc_timezone_validator
52
+ convert_to_utc_datetime
53
53
  )
54
54
  _add_timezone_validator_end_time = field_validator("end_time")(
55
- add_utc_timezone_validator
55
+ convert_to_utc_datetime
56
56
  )
57
57
 
58
58
  def model_dump_prefixed(self) -> dict[str, t.Any]:
@@ -28,8 +28,8 @@ from prediction_market_agent_tooling.monitor.monitor import (
28
28
  from prediction_market_agent_tooling.monitor.monitor_settings import MonitorSettings
29
29
  from prediction_market_agent_tooling.tools.utils import (
30
30
  DatetimeWithTimezone,
31
- add_utc_timezone_validator,
32
31
  check_not_none,
32
+ convert_to_utc_datetime,
33
33
  utcnow,
34
34
  )
35
35
 
@@ -104,7 +104,7 @@ def monitor_app(
104
104
  st.selectbox(label="Market type", options=enabled_market_types, index=0)
105
105
  )
106
106
  start_time: DatetimeWithTimezone | None = (
107
- add_utc_timezone_validator(
107
+ convert_to_utc_datetime(
108
108
  datetime.combine(
109
109
  t.cast(
110
110
  # This will be always a date for us, so casting.
@@ -6,6 +6,7 @@ from contextlib import contextmanager
6
6
 
7
7
  from pydantic import BaseModel, field_validator
8
8
  from web3 import Web3
9
+ from web3.contract.contract import Contract as Web3Contract
9
10
 
10
11
  from prediction_market_agent_tooling.config import APIKeys
11
12
  from prediction_market_agent_tooling.gtypes import (
@@ -21,7 +22,11 @@ from prediction_market_agent_tooling.tools.gnosis_rpc import (
21
22
  GNOSIS_NETWORK_ID,
22
23
  GNOSIS_RPC_URL,
23
24
  )
24
- from prediction_market_agent_tooling.tools.utils import should_not_happen
25
+ from prediction_market_agent_tooling.tools.utils import (
26
+ DatetimeWithTimezone,
27
+ should_not_happen,
28
+ utc_timestamp_to_utc_datetime,
29
+ )
25
30
  from prediction_market_agent_tooling.tools.web3_utils import (
26
31
  call_function_on_contract,
27
32
  send_function_on_contract_tx,
@@ -73,6 +78,10 @@ class ContractBaseClass(BaseModel):
73
78
  {}
74
79
  ) # Can be used to hold values that aren't going to change after getting them for the first time, as for example `symbol` of an ERC-20 token.
75
80
 
81
+ def get_web3_contract(self, web3: Web3 | None = None) -> Web3Contract:
82
+ web3 = web3 or self.get_web3()
83
+ return web3.eth.contract(address=self.address, abi=self.abi)
84
+
76
85
  def call(
77
86
  self,
78
87
  function_name: str,
@@ -423,6 +432,46 @@ class ContractERC4626OnGnosisChain(
423
432
  return to_gnosis_chain_contract(super().get_asset_token_contract(web3=web3))
424
433
 
425
434
 
435
+ class DebuggingContract(ContractOnGnosisChain):
436
+ # Contract ABI taken from https://gnosisscan.io/address/0x5Aa82E068aE6a6a1C26c42E5a59520a74Cdb8998#code.
437
+ abi: ABI = abi_field_validator(
438
+ os.path.join(
439
+ os.path.dirname(os.path.realpath(__file__)),
440
+ "../abis/debuggingcontract.abi.json",
441
+ )
442
+ )
443
+ address: ChecksumAddress = Web3.to_checksum_address(
444
+ "0x5Aa82E068aE6a6a1C26c42E5a59520a74Cdb8998"
445
+ )
446
+
447
+ def getNow(
448
+ self,
449
+ web3: Web3 | None = None,
450
+ ) -> int:
451
+ now: int = self.call(
452
+ function_name="getNow",
453
+ web3=web3,
454
+ )
455
+ return now
456
+
457
+ def get_now(
458
+ self,
459
+ web3: Web3 | None = None,
460
+ ) -> DatetimeWithTimezone:
461
+ return utc_timestamp_to_utc_datetime(self.getNow(web3))
462
+
463
+ def inc(
464
+ self,
465
+ api_keys: APIKeys,
466
+ web3: Web3 | None = None,
467
+ ) -> TxReceipt:
468
+ return self.send(
469
+ api_keys=api_keys,
470
+ function_name="inc",
471
+ web3=web3,
472
+ )
473
+
474
+
426
475
  def contract_implements_function(
427
476
  contract_address: ChecksumAddress,
428
477
  function_name: str,
@@ -0,0 +1,11 @@
1
+ import hishel
2
+
3
+
4
+ class HttpxCachedClient:
5
+ def __init__(self) -> None:
6
+ storage = hishel.FileStorage(ttl=3600, check_ttl_every=600)
7
+ controller = hishel.Controller(force_cache=True)
8
+ self.client = hishel.CacheClient(storage=storage, controller=controller)
9
+
10
+ def get_client(self) -> hishel.CacheClient:
11
+ return self.client
@@ -0,0 +1,33 @@
1
+ import json
2
+ import tempfile
3
+
4
+ from pinatapy import PinataPy
5
+
6
+ from prediction_market_agent_tooling.config import APIKeys
7
+ from prediction_market_agent_tooling.gtypes import IPFSCIDVersion0
8
+ from prediction_market_agent_tooling.markets.omen.data_models import IPFSAgentResult
9
+
10
+
11
+ class IPFSHandler:
12
+ def __init__(self, api_keys: APIKeys):
13
+ self.pinata = PinataPy(
14
+ api_keys.pinata_api_key.get_secret_value(),
15
+ api_keys.pinata_api_secret.get_secret_value(),
16
+ )
17
+
18
+ def upload_file(self, file_path: str) -> IPFSCIDVersion0:
19
+ return IPFSCIDVersion0(
20
+ self.pinata.pin_file_to_ipfs(file_path, save_absolute_paths=False)[
21
+ "IpfsHash"
22
+ ]
23
+ )
24
+
25
+ def store_agent_result(self, agent_result: IPFSAgentResult) -> IPFSCIDVersion0:
26
+ with tempfile.NamedTemporaryFile(mode="r+", encoding="utf-8") as json_file:
27
+ json.dump(agent_result.model_dump(), json_file, indent=4)
28
+ json_file.flush()
29
+ ipfs_hash = self.upload_file(json_file.name)
30
+ return ipfs_hash
31
+
32
+ def unpin_file(self, hash_to_remove: str) -> None:
33
+ self.pinata.remove_pin_from_ipfs(hash_to_remove=hash_to_remove)
@@ -1,7 +1,7 @@
1
1
  import tenacity
2
- from loguru import logger
3
2
 
4
3
  from prediction_market_agent_tooling.config import APIKeys
4
+ from prediction_market_agent_tooling.loggers import logger
5
5
  from prediction_market_agent_tooling.tools.cache import persistent_inmemory_cache
6
6
  from prediction_market_agent_tooling.tools.langfuse_ import (
7
7
  get_langfuse_langchain_config,
@@ -6,6 +6,7 @@ from langfuse import Langfuse
6
6
  from langfuse.client import TraceWithDetails
7
7
  from pydantic import BaseModel
8
8
 
9
+ from prediction_market_agent_tooling.loggers import logger
9
10
  from prediction_market_agent_tooling.markets.data_models import (
10
11
  PlacedTrade,
11
12
  ProbabilisticAnswer,
@@ -13,7 +14,7 @@ from prediction_market_agent_tooling.markets.data_models import (
13
14
  TradeType,
14
15
  )
15
16
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
16
- from prediction_market_agent_tooling.tools.utils import add_utc_timezone_validator
17
+ from prediction_market_agent_tooling.tools.utils import convert_to_utc_datetime
17
18
 
18
19
 
19
20
  class ProcessMarketTrace(BaseModel):
@@ -23,11 +24,11 @@ class ProcessMarketTrace(BaseModel):
23
24
  trades: list[PlacedTrade]
24
25
 
25
26
  @property
26
- def buy_trade(self) -> PlacedTrade:
27
+ def buy_trade(self) -> PlacedTrade | None:
27
28
  buy_trades = [t for t in self.trades if t.trade_type == TradeType.BUY]
28
- if len(buy_trades) == 1:
29
- return buy_trades[0]
30
- raise ValueError("No buy trade found")
29
+ if len(buy_trades) > 1:
30
+ raise ValueError("Unhandled logic, check it outm please!")
31
+ return buy_trades[0] if buy_trades else None
31
32
 
32
33
  @staticmethod
33
34
  def from_langfuse_trace(
@@ -134,8 +135,10 @@ def get_trace_for_bet(
134
135
  traces_for_bet: list[ProcessMarketTrace] = []
135
136
  for t in traces:
136
137
  # Cannot use exact comparison due to gas fees
137
- if t.buy_trade.outcome == bet.outcome and np.isclose(
138
- t.buy_trade.amount.amount, bet.amount.amount
138
+ if (
139
+ t.buy_trade
140
+ and t.buy_trade.outcome == bet.outcome
141
+ and np.isclose(t.buy_trade.amount.amount, bet.amount.amount)
139
142
  ):
140
143
  traces_for_bet.append(t)
141
144
 
@@ -146,9 +149,21 @@ def get_trace_for_bet(
146
149
  else:
147
150
  # In-case there are multiple traces for the same market, get the closest
148
151
  # trace to the bet
152
+ bet_timestamp = convert_to_utc_datetime(bet.created_time)
149
153
  closest_trace_index = get_closest_datetime_from_list(
150
- add_utc_timezone_validator(bet.created_time),
154
+ bet_timestamp,
151
155
  [t.timestamp for t in traces_for_bet],
152
156
  )
153
157
 
158
+ # Sanity check: Let's say the upper bound for time between
159
+ # `agent.process_market` being called and the bet being placed is 20
160
+ # minutes
161
+ candidate_trace = traces_for_bet[closest_trace_index]
162
+ if abs(candidate_trace.timestamp - bet_timestamp).total_seconds() > 1200:
163
+ logger.info(
164
+ f"Closest trace to bet has timestamp {candidate_trace.timestamp}, "
165
+ f"but bet was created at {bet_timestamp}. Not matching"
166
+ )
167
+ return None
168
+
154
169
  return traces_for_bet[closest_trace_index]