prediction-market-agent-tooling 0.48.18__py3-none-any.whl → 0.49.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +86 -13
  6. prediction_market_agent_tooling/deploy/betting_strategy.py +5 -35
  7. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -1
  8. prediction_market_agent_tooling/markets/agent_market.py +14 -6
  9. prediction_market_agent_tooling/markets/data_models.py +14 -0
  10. prediction_market_agent_tooling/markets/manifold/api.py +3 -1
  11. prediction_market_agent_tooling/markets/manifold/manifold.py +7 -2
  12. prediction_market_agent_tooling/markets/metaculus/metaculus.py +6 -1
  13. prediction_market_agent_tooling/markets/omen/data_models.py +247 -6
  14. prediction_market_agent_tooling/markets/omen/omen.py +77 -43
  15. prediction_market_agent_tooling/markets/omen/omen_contracts.py +179 -33
  16. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +35 -0
  17. prediction_market_agent_tooling/markets/polymarket/polymarket.py +1 -1
  18. prediction_market_agent_tooling/monitor/markets/polymarket.py +4 -0
  19. prediction_market_agent_tooling/monitor/monitor.py +3 -3
  20. prediction_market_agent_tooling/monitor/monitor_app.py +2 -2
  21. prediction_market_agent_tooling/tools/contract.py +50 -1
  22. prediction_market_agent_tooling/tools/ipfs/ipfs_handler.py +33 -0
  23. prediction_market_agent_tooling/tools/langfuse_client_utils.py +27 -12
  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.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/METADATA +2 -1
  27. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/RECORD +30 -27
  28. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/LICENSE +0 -0
  29. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/WHEEL +0 -0
  30. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.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,7 @@ 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.
416
434
 
417
435
 
418
436
  class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
@@ -433,14 +451,15 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
433
451
  condition_id: HexBytes,
434
452
  initial_funds_wei: Wei,
435
453
  collateral_token_address: ChecksumAddress,
436
- fee: float = OMEN_DEFAULT_MARKET_FEE,
454
+ fee: Wei, # This is actually fee in %, 'where 100% == 1 xDai'.
455
+ distribution_hint: list[OmenOutcomeToken] | None = None,
437
456
  tx_params: t.Optional[TxParams] = None,
438
457
  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(
458
+ ) -> tuple[
459
+ OmenFixedProductMarketMakerCreationEvent, FPMMFundingAddedEvent, TxReceipt
460
+ ]:
461
+ web3 = web3 or self.get_web3()
462
+ receipt_tx = self.send(
444
463
  api_keys=api_keys,
445
464
  function_name="create2FixedProductMarketMaker",
446
465
  function_params=dict(
@@ -450,19 +469,41 @@ class OmenFixedProductMarketMakerFactoryContract(ContractOnGnosisChain):
450
469
  conditionalTokens=OmenConditionalTokenContract().address,
451
470
  collateralToken=collateral_token_address,
452
471
  conditionIds=[condition_id],
453
- fee=fee_wei,
472
+ fee=fee,
454
473
  initialFunds=initial_funds_wei,
455
- distributionHint=[],
474
+ distributionHint=distribution_hint or [],
456
475
  ),
457
476
  tx_params=tx_params,
458
477
  web3=web3,
459
478
  )
460
479
 
480
+ market_event_logs = (
481
+ self.get_web3_contract(web3=web3)
482
+ .events.FixedProductMarketMakerCreation()
483
+ .process_receipt(receipt_tx)
484
+ )
485
+ market_event = OmenFixedProductMarketMakerCreationEvent(
486
+ **market_event_logs[0]["args"]
487
+ )
488
+ funding_event_logs = (
489
+ self.get_web3_contract(web3=web3)
490
+ .events.FPMMFundingAdded()
491
+ .process_receipt(receipt_tx)
492
+ )
493
+ funding_event = FPMMFundingAddedEvent(**funding_event_logs[0]["args"])
494
+
495
+ return market_event, funding_event, receipt_tx
496
+
461
497
 
462
498
  class Arbitrator(str, Enum):
463
- KLEROS = "kleros"
499
+ KLEROS_511_JURORS_WITHOUT_APPEAL = "kleros_511_jurors_without_appeal"
500
+ KLEROS_31_JURORS_WITH_APPEAL = "kleros_31_jurors_with_appeal"
464
501
  DXDAO = "dxdao"
465
502
 
503
+ @property
504
+ def is_kleros(self) -> bool:
505
+ return self.value.startswith("kleros")
506
+
466
507
 
467
508
  class OmenDxDaoContract(ContractOnGnosisChain):
468
509
  # Contract ABI taken from https://gnosisscan.io/address/0xFe14059344b74043Af518d12931600C0f52dF7c5#code.
@@ -485,9 +526,22 @@ class OmenKlerosContract(ContractOnGnosisChain):
485
526
  "../../abis/omen_kleros.abi.json",
486
527
  )
487
528
  )
488
- address: ChecksumAddress = Web3.to_checksum_address(
489
- "0xe40DD83a262da3f56976038F1554Fe541Fa75ecd"
490
- )
529
+
530
+ @staticmethod
531
+ def from_arbitrator(arbitrator: "Arbitrator") -> "OmenKlerosContract":
532
+ """
533
+ See https://docs.kleros.io/developer/deployment-addresses for all available addresses.
534
+ """
535
+ if arbitrator == Arbitrator.KLEROS_511_JURORS_WITHOUT_APPEAL:
536
+ address = "0xe40DD83a262da3f56976038F1554Fe541Fa75ecd"
537
+
538
+ elif arbitrator == Arbitrator.KLEROS_31_JURORS_WITH_APPEAL:
539
+ address = "0x29f39de98d750eb77b5fafb31b2837f079fce222"
540
+
541
+ else:
542
+ raise ValueError(f"Unsupported arbitrator: {arbitrator=}")
543
+
544
+ return OmenKlerosContract(address=Web3.to_checksum_address(address))
491
545
 
492
546
 
493
547
  class OmenRealitioContract(ContractOnGnosisChain):
@@ -506,8 +560,8 @@ class OmenRealitioContract(ContractOnGnosisChain):
506
560
  def get_arbitrator_contract(
507
561
  arbitrator: Arbitrator,
508
562
  ) -> ContractOnGnosisChain:
509
- if arbitrator == Arbitrator.KLEROS:
510
- return OmenKlerosContract()
563
+ if arbitrator.is_kleros:
564
+ return OmenKlerosContract.from_arbitrator(arbitrator)
511
565
  if arbitrator == Arbitrator.DXDAO:
512
566
  return OmenDxDaoContract()
513
567
  raise ValueError(f"Unknown arbitrator: {arbitrator}")
@@ -521,25 +575,25 @@ class OmenRealitioContract(ContractOnGnosisChain):
521
575
  language: str,
522
576
  arbitrator: Arbitrator,
523
577
  opening: datetime,
524
- timeout: timedelta = timedelta(days=1),
578
+ timeout: timedelta,
525
579
  nonce: int | None = None,
526
580
  tx_params: t.Optional[TxParams] = None,
527
581
  web3: Web3 | None = None,
528
- ) -> HexBytes:
582
+ ) -> RealitioLogNewQuestionEvent:
529
583
  """
530
584
  After the question is created, you can find it at https://reality.eth.link/app/#!/creator/{from_address}.
531
585
  """
586
+ web3 = web3 or self.get_web3()
532
587
  arbitrator_contract_address = self.get_arbitrator_contract(arbitrator).address
533
588
  # See https://realitio.github.io/docs/html/contracts.html#templates
534
589
  # for possible template ids and how to format the question.
535
590
  template_id = 2
536
- realitio_question = "␟".join(
537
- [
538
- question,
539
- ",".join(f'"{o}"' for o in outcomes),
540
- category,
541
- language,
542
- ]
591
+ realitio_question = format_realitio_question(
592
+ question=question,
593
+ outcomes=outcomes,
594
+ category=category,
595
+ language=language,
596
+ template_id=template_id,
543
597
  )
544
598
  receipt_tx = self.send(
545
599
  api_keys=api_keys,
@@ -557,10 +611,15 @@ class OmenRealitioContract(ContractOnGnosisChain):
557
611
  tx_params=tx_params,
558
612
  web3=web3,
559
613
  )
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
614
+
615
+ event_logs = (
616
+ self.get_web3_contract(web3=web3)
617
+ .events.LogNewQuestion()
618
+ .process_receipt(receipt_tx)
619
+ )
620
+ question_event = RealitioLogNewQuestionEvent(**event_logs[0]["args"])
621
+
622
+ return question_event
564
623
 
565
624
  def submitAnswer(
566
625
  self,
@@ -670,6 +729,93 @@ class OmenRealitioContract(ContractOnGnosisChain):
670
729
  ) -> TxReceipt:
671
730
  return self.send(api_keys=api_keys, function_name="withdraw", web3=web3)
672
731
 
732
+ def getOpeningTS(
733
+ self,
734
+ question_id: HexBytes,
735
+ web3: Web3 | None = None,
736
+ ) -> int:
737
+ ts: int = self.call(
738
+ function_name="getOpeningTS",
739
+ function_params=[question_id],
740
+ web3=web3,
741
+ )
742
+ return ts
743
+
744
+ def getFinalizeTS(
745
+ self,
746
+ question_id: HexBytes,
747
+ web3: Web3 | None = None,
748
+ ) -> int:
749
+ ts: int = self.call(
750
+ function_name="getFinalizeTS",
751
+ function_params=[question_id],
752
+ web3=web3,
753
+ )
754
+ return ts
755
+
756
+ def isFinalized(
757
+ self,
758
+ question_id: HexBytes,
759
+ web3: Web3 | None = None,
760
+ ) -> bool:
761
+ is_finalized: bool = self.call(
762
+ function_name="isFinalized",
763
+ function_params=[question_id],
764
+ web3=web3,
765
+ )
766
+ return is_finalized
767
+
768
+ def isPendingArbitration(
769
+ self,
770
+ question_id: HexBytes,
771
+ web3: Web3 | None = None,
772
+ ) -> bool:
773
+ is_pending_arbitration: bool = self.call(
774
+ function_name="isPendingArbitration",
775
+ function_params=[question_id],
776
+ web3=web3,
777
+ )
778
+ return is_pending_arbitration
779
+
780
+
781
+ class OmenAgentResultMappingContract(ContractOnGnosisChain):
782
+ # Contract ABI taken from built https://github.com/gnosis/labs-contracts.
783
+
784
+ abi: ABI = abi_field_validator(
785
+ os.path.join(
786
+ os.path.dirname(os.path.realpath(__file__)),
787
+ "../../abis/omen_agentresultmapping.abi.json",
788
+ )
789
+ )
790
+
791
+ address: ChecksumAddress = Web3.to_checksum_address(
792
+ "0x260E1077dEA98e738324A6cEfB0EE9A272eD471a"
793
+ )
794
+
795
+ def get_predictions(
796
+ self,
797
+ market_address: ChecksumAddress,
798
+ web3: Web3 | None = None,
799
+ ) -> list[ContractPrediction]:
800
+ prediction_tuples = self.call(
801
+ "getPredictions", function_params=[market_address], web3=web3
802
+ )
803
+ return [ContractPrediction.from_tuple(p) for p in prediction_tuples]
804
+
805
+ def add_prediction(
806
+ self,
807
+ api_keys: APIKeys,
808
+ market_address: ChecksumAddress,
809
+ prediction: ContractPrediction,
810
+ web3: Web3 | None = None,
811
+ ) -> TxReceipt:
812
+ return self.send(
813
+ api_keys=api_keys,
814
+ function_name="addPrediction",
815
+ function_params=[market_address, prediction.model_dump(by_alias=True)],
816
+ web3=web3,
817
+ )
818
+
673
819
 
674
820
  class OmenThumbnailMapping(ContractOnGnosisChain):
675
821
  # Contract ABI taken from built https://github.com/gnosis/labs-contracts.
@@ -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)
@@ -797,3 +806,29 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
797
806
  if image_url is not None
798
807
  else None
799
808
  )
809
+
810
+ def get_agent_results_for_market(
811
+ self, market_id: HexAddress | None = None
812
+ ) -> list[ContractPrediction]:
813
+ where_stms = {}
814
+ if market_id:
815
+ where_stms["marketAddress"] = market_id.lower()
816
+
817
+ prediction_added = (
818
+ self.omen_agent_result_mapping_subgraph.Query.predictionAddeds(
819
+ where=where_stms,
820
+ orderBy="blockNumber",
821
+ orderDirection="asc",
822
+ )
823
+ )
824
+ fields = [
825
+ prediction_added.publisherAddress,
826
+ prediction_added.ipfsHash,
827
+ prediction_added.txHashes,
828
+ prediction_added.estimatedProbabilityBps,
829
+ ]
830
+ result = self.sg.query_json(fields)
831
+ items = self._parse_items_from_json(result)
832
+ if not items:
833
+ return []
834
+ return [ContractPrediction.model_validate(i) for i in items]
@@ -46,7 +46,7 @@ class PolymarketAgentMarket(AgentMarket):
46
46
  def get_tiny_bet_amount(cls) -> BetAmount:
47
47
  raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
48
48
 
49
- def place_bet(self, outcome: bool, amount: BetAmount) -> None:
49
+ def place_bet(self, outcome: bool, amount: BetAmount) -> str:
50
50
  raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
51
51
 
52
52
  @staticmethod
@@ -43,3 +43,7 @@ class DeployedPolymarketAgent(DeployedAgent):
43
43
  == MarketType.POLYMARKET.value,
44
44
  ) -> t.Sequence["DeployedPolymarketAgent"]:
45
45
  return super().from_all_gcp_functions(filter_=filter_)
46
+
47
+ @staticmethod
48
+ def get_user_id(api_keys: APIKeys) -> str:
49
+ return api_keys.bet_from_address
@@ -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,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)
@@ -6,28 +6,29 @@ 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 (
11
+ PlacedTrade,
10
12
  ProbabilisticAnswer,
11
13
  ResolvedBet,
12
- Trade,
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):
20
21
  timestamp: datetime
21
22
  market: OmenAgentMarket
22
23
  answer: ProbabilisticAnswer
23
- trades: list[Trade]
24
+ trades: list[PlacedTrade]
24
25
 
25
26
  @property
26
- def buy_trade(self) -> Trade:
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(
@@ -107,10 +108,10 @@ def trace_to_answer(trace: TraceWithDetails) -> ProbabilisticAnswer:
107
108
  return ProbabilisticAnswer.model_validate(trace.output["answer"])
108
109
 
109
110
 
110
- def trace_to_trades(trace: TraceWithDetails) -> list[Trade]:
111
+ def trace_to_trades(trace: TraceWithDetails) -> list[PlacedTrade]:
111
112
  assert trace.output is not None, "Trace output is None"
112
113
  assert trace.output["trades"] is not None, "Trace output trades is None"
113
- return [Trade.model_validate(t) for t in trace.output["trades"]]
114
+ return [PlacedTrade.model_validate(t) for t in trace.output["trades"]]
114
115
 
115
116
 
116
117
  def get_closest_datetime_from_list(
@@ -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]