prediction-market-agent-tooling 0.57.16.dev311__py3-none-any.whl → 0.57.18__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 (23) hide show
  1. prediction_market_agent_tooling/abis/seer_market_factory.abi.json +609 -0
  2. prediction_market_agent_tooling/config.py +8 -0
  3. prediction_market_agent_tooling/loggers.py +9 -1
  4. prediction_market_agent_tooling/markets/omen/omen.py +12 -16
  5. prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
  6. prediction_market_agent_tooling/markets/omen/omen_contracts.py +16 -4
  7. prediction_market_agent_tooling/markets/omen/omen_resolving.py +3 -3
  8. prediction_market_agent_tooling/markets/seer/data_models.py +41 -2
  9. prediction_market_agent_tooling/markets/seer/seer.py +94 -0
  10. prediction_market_agent_tooling/markets/seer/seer_contracts.py +76 -0
  11. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +2 -1
  12. prediction_market_agent_tooling/monitor/monitor.py +1 -1
  13. prediction_market_agent_tooling/monitor/monitor_app.py +4 -8
  14. prediction_market_agent_tooling/tools/contract.py +1 -98
  15. prediction_market_agent_tooling/tools/cow/cow_order.py +133 -0
  16. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +156 -0
  17. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +62 -0
  18. prediction_market_agent_tooling/tools/tokens/main_token.py +13 -0
  19. {prediction_market_agent_tooling-0.57.16.dev311.dist-info → prediction_market_agent_tooling-0.57.18.dist-info}/METADATA +3 -1
  20. {prediction_market_agent_tooling-0.57.16.dev311.dist-info → prediction_market_agent_tooling-0.57.18.dist-info}/RECORD +23 -15
  21. {prediction_market_agent_tooling-0.57.16.dev311.dist-info → prediction_market_agent_tooling-0.57.18.dist-info}/LICENSE +0 -0
  22. {prediction_market_agent_tooling-0.57.16.dev311.dist-info → prediction_market_agent_tooling-0.57.18.dist-info}/WHEEL +0 -0
  23. {prediction_market_agent_tooling-0.57.16.dev311.dist-info → prediction_market_agent_tooling-0.57.18.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,8 @@
1
+ from web3 import Web3
2
+
3
+ WRAPPED_XDAI_CONTRACT_ADDRESS = Web3.to_checksum_address(
4
+ "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"
5
+ )
6
+ SDAI_CONTRACT_ADDRESS = Web3.to_checksum_address(
7
+ "0xaf204776c7245bF4147c2612BF6e5972Ee483701"
8
+ )
@@ -31,6 +31,10 @@ from prediction_market_agent_tooling.markets.omen.data_models import (
31
31
  RealitioLogNewQuestionEvent,
32
32
  format_realitio_question,
33
33
  )
34
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
35
+ SDAI_CONTRACT_ADDRESS,
36
+ WRAPPED_XDAI_CONTRACT_ADDRESS,
37
+ )
34
38
  from prediction_market_agent_tooling.tools.contract import (
35
39
  ContractDepositableWrapperERC20OnGnosisChain,
36
40
  ContractERC20OnGnosisChain,
@@ -419,18 +423,26 @@ class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
419
423
  )
420
424
 
421
425
 
422
- class WrappedxDaiContract(ContractDepositableWrapperERC20OnGnosisChain):
426
+ class GNOContract(ContractERC20OnGnosisChain):
423
427
  address: ChecksumAddress = Web3.to_checksum_address(
424
- "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"
428
+ "0x9c58bacc331c9aa871afd802db6379a98e80cedb"
425
429
  )
426
430
 
427
431
 
428
- class sDaiContract(ContractERC4626OnGnosisChain):
432
+ class WETHContract(ContractERC20OnGnosisChain):
429
433
  address: ChecksumAddress = Web3.to_checksum_address(
430
- "0xaf204776c7245bF4147c2612BF6e5972Ee483701"
434
+ "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1"
431
435
  )
432
436
 
433
437
 
438
+ class WrappedxDaiContract(ContractDepositableWrapperERC20OnGnosisChain):
439
+ address: ChecksumAddress = WRAPPED_XDAI_CONTRACT_ADDRESS
440
+
441
+
442
+ class sDaiContract(ContractERC4626OnGnosisChain):
443
+ address: ChecksumAddress = SDAI_CONTRACT_ADDRESS
444
+
445
+
434
446
  OMEN_DEFAULT_MARKET_FEE_PERC = 0.02 # 2% fee from the buying shares amount.
435
447
  REALITY_DEFAULT_FINALIZATION_TIMEOUT = timedelta(days=3)
436
448
 
@@ -50,7 +50,7 @@ def claim_bonds_on_realitio_questions(
50
50
  except Exception as e:
51
51
  if not skip_failed:
52
52
  raise e
53
- logger.error(
53
+ logger.warning(
54
54
  f"Failed to claim bond for {question.url=}, {question.questionId=}: {e}"
55
55
  )
56
56
 
@@ -203,7 +203,7 @@ def omen_submit_answer_market_tx(
203
203
  web3: Web3 | None = None,
204
204
  ) -> None:
205
205
  """
206
- After the answer is submitted, there is 24h waiting period where the answer can be challenged by others.
206
+ After the answer is submitted, there is waiting period where the answer can be challenged by others.
207
207
  And after the period is over, you need to resolve the market using `omen_resolve_market_tx`.
208
208
  """
209
209
  realitio_contract = OmenRealitioContract()
@@ -224,7 +224,7 @@ def omen_submit_invalid_answer_market_tx(
224
224
  web3: Web3 | None = None,
225
225
  ) -> None:
226
226
  """
227
- After the answer is submitted, there is 24h waiting period where the answer can be challenged by others.
227
+ After the answer is submitted, there is waiting period where the answer can be challenged by others.
228
228
  And after the period is over, you need to resolve the market using `omen_resolve_market_tx`.
229
229
  """
230
230
  realitio_contract = OmenRealitioContract()
@@ -1,6 +1,33 @@
1
+ import typing as t
2
+
3
+ from eth_typing import HexAddress
1
4
  from pydantic import BaseModel, ConfigDict, Field
5
+ from web3.constants import ADDRESS_ZERO
6
+
7
+ from prediction_market_agent_tooling.gtypes import HexBytes, Wei
8
+
9
+
10
+ class CreateCategoricalMarketsParams(BaseModel):
11
+ model_config = ConfigDict(populate_by_name=True)
2
12
 
3
- from prediction_market_agent_tooling.gtypes import HexBytes
13
+ market_name: str = Field(..., alias="marketName")
14
+ outcomes: list[str]
15
+ # Only relevant for scalar markets
16
+ question_start: str = Field(alias="questionStart", default="")
17
+ question_end: str = Field(alias="questionEnd", default="")
18
+ outcome_type: str = Field(alias="outcomeType", default="")
19
+
20
+ # Not needed for non-conditional markets.
21
+ parent_outcome: int = Field(alias="parentOutcome", default=0)
22
+ parent_market: HexAddress = Field(alias="parentMarket", default=ADDRESS_ZERO)
23
+
24
+ category: str
25
+ lang: str
26
+ lower_bound: int = Field(alias="lowerBound", default=0)
27
+ upper_bound: int = Field(alias="upperBound", default=0)
28
+ min_bond: Wei = Field(..., alias="minBond")
29
+ opening_time: int = Field(..., alias="openingTime")
30
+ token_names: list[str] = Field(..., alias="tokenNames")
4
31
 
5
32
 
6
33
  class SeerParentMarket(BaseModel):
@@ -13,8 +40,11 @@ class SeerMarket(BaseModel):
13
40
  id: HexBytes
14
41
  title: str = Field(alias="marketName")
15
42
  outcomes: list[str]
16
- parent_market: SeerParentMarket | None = Field(alias="parentMarket")
17
43
  wrapped_tokens: list[HexBytes] = Field(alias="wrappedTokens")
44
+ parent_outcome: int = Field(alias="parentOutcome")
45
+ parent_market: t.Optional[SeerParentMarket] = Field(
46
+ alias="parentMarket", default=None
47
+ )
18
48
 
19
49
 
20
50
  class SeerToken(BaseModel):
@@ -29,3 +59,12 @@ class SeerPool(BaseModel):
29
59
  liquidity: int
30
60
  token0: SeerToken
31
61
  token1: SeerToken
62
+
63
+
64
+ class NewMarketEvent(BaseModel):
65
+ market: HexAddress
66
+ marketName: str
67
+ parentMarket: HexAddress
68
+ conditionId: HexBytes
69
+ questionId: HexBytes
70
+ questionsIds: list[HexBytes]
@@ -0,0 +1,94 @@
1
+ from eth_typing import ChecksumAddress
2
+ from web3 import Web3
3
+ from web3.types import TxReceipt
4
+
5
+ from prediction_market_agent_tooling.config import APIKeys
6
+ from prediction_market_agent_tooling.gtypes import xDai
7
+ from prediction_market_agent_tooling.markets.seer.data_models import NewMarketEvent
8
+ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
9
+ SeerMarketFactory,
10
+ )
11
+ from prediction_market_agent_tooling.tools.contract import (
12
+ init_collateral_token_contract,
13
+ to_gnosis_chain_contract,
14
+ )
15
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
16
+ from prediction_market_agent_tooling.tools.tokens.auto_deposit import (
17
+ auto_deposit_collateral_token,
18
+ )
19
+ from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei
20
+
21
+
22
+ def seer_create_market_tx(
23
+ api_keys: APIKeys,
24
+ initial_funds: xDai,
25
+ question: str,
26
+ opening_time: DatetimeUTC,
27
+ language: str,
28
+ outcomes: list[str],
29
+ auto_deposit: bool,
30
+ category: str,
31
+ min_bond_xdai: xDai,
32
+ web3: Web3 | None = None,
33
+ ) -> ChecksumAddress:
34
+ web3 = web3 or SeerMarketFactory.get_web3() # Default to Gnosis web3.
35
+ initial_funds_wei = xdai_to_wei(initial_funds)
36
+
37
+ factory_contract = SeerMarketFactory()
38
+ collateral_token_address = factory_contract.collateral_token(web3=web3)
39
+ collateral_token_contract = to_gnosis_chain_contract(
40
+ init_collateral_token_contract(collateral_token_address, web3)
41
+ )
42
+
43
+ if auto_deposit:
44
+ auto_deposit_collateral_token(
45
+ collateral_token_contract=collateral_token_contract,
46
+ api_keys=api_keys,
47
+ amount_wei=initial_funds_wei,
48
+ web3=web3,
49
+ )
50
+
51
+ # In case of ERC4626, obtained (for example) sDai out of xDai could be lower than the `amount_wei`, so we need to handle it.
52
+ initial_funds_in_shares = collateral_token_contract.get_in_shares(
53
+ amount=initial_funds_wei, web3=web3
54
+ )
55
+
56
+ # Approve the market maker to withdraw our collateral token.
57
+ collateral_token_contract.approve(
58
+ api_keys=api_keys,
59
+ for_address=factory_contract.address,
60
+ amount_wei=initial_funds_in_shares,
61
+ web3=web3,
62
+ )
63
+
64
+ # Create the market.
65
+ params = factory_contract.build_market_params(
66
+ market_question=question,
67
+ outcomes=outcomes,
68
+ opening_time=opening_time,
69
+ language=language,
70
+ category=category,
71
+ min_bond_xdai=min_bond_xdai,
72
+ )
73
+ tx_receipt = factory_contract.create_categorical_market(
74
+ api_keys=api_keys, params=params, web3=web3
75
+ )
76
+
77
+ # ToDo - Add liquidity to market on Swapr (https://github.com/gnosis/prediction-market-agent-tooling/issues/497)
78
+ market_address = extract_market_address_from_tx(
79
+ factory_contract=factory_contract, tx_receipt=tx_receipt, web3=web3
80
+ )
81
+ return market_address
82
+
83
+
84
+ def extract_market_address_from_tx(
85
+ factory_contract: SeerMarketFactory, tx_receipt: TxReceipt, web3: Web3
86
+ ) -> ChecksumAddress:
87
+ """We extract the newly created market from the NewMarket event emitted in the transaction."""
88
+ event_logs = (
89
+ factory_contract.get_web3_contract(web3=web3)
90
+ .events.NewMarket()
91
+ .process_receipt(tx_receipt)
92
+ )
93
+ new_market_event = NewMarketEvent(**event_logs[0]["args"])
94
+ return Web3.to_checksum_address(new_market_event.market)
@@ -0,0 +1,76 @@
1
+ import os
2
+
3
+ from web3 import Web3
4
+ from web3.types import TxReceipt
5
+
6
+ from prediction_market_agent_tooling.config import APIKeys
7
+ from prediction_market_agent_tooling.gtypes import ABI, ChecksumAddress, xDai
8
+ from prediction_market_agent_tooling.markets.seer.data_models import (
9
+ CreateCategoricalMarketsParams,
10
+ )
11
+ from prediction_market_agent_tooling.tools.contract import (
12
+ ContractOnGnosisChain,
13
+ abi_field_validator,
14
+ )
15
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
16
+ from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei
17
+
18
+
19
+ class SeerMarketFactory(ContractOnGnosisChain):
20
+ # https://gnosisscan.io/address/0x83183da839ce8228e31ae41222ead9edbb5cdcf1#code.
21
+ abi: ABI = abi_field_validator(
22
+ os.path.join(
23
+ os.path.dirname(os.path.realpath(__file__)),
24
+ "../../abis/seer_market_factory.abi.json",
25
+ )
26
+ )
27
+ address: ChecksumAddress = Web3.to_checksum_address(
28
+ "0x83183da839ce8228e31ae41222ead9edbb5cdcf1"
29
+ )
30
+
31
+ @staticmethod
32
+ def build_market_params(
33
+ market_question: str,
34
+ outcomes: list[str],
35
+ opening_time: DatetimeUTC,
36
+ min_bond_xdai: xDai,
37
+ language: str = "en_US",
38
+ category: str = "misc",
39
+ ) -> CreateCategoricalMarketsParams:
40
+ return CreateCategoricalMarketsParams(
41
+ market_name=market_question,
42
+ token_names=[
43
+ o.upper() for o in outcomes
44
+ ], # Following usual token names on Seer (YES,NO).
45
+ min_bond=xdai_to_wei(min_bond_xdai),
46
+ opening_time=int(opening_time.timestamp()),
47
+ outcomes=outcomes,
48
+ lang=language,
49
+ category=category,
50
+ )
51
+
52
+ def market_count(self, web3: Web3 | None = None) -> int:
53
+ count: int = self.call("marketCount", web3=web3)
54
+ return count
55
+
56
+ def market_at_index(self, index: int, web3: Web3 | None = None) -> ChecksumAddress:
57
+ market_address: str = self.call("markets", function_params=[index], web3=web3)
58
+ return Web3.to_checksum_address(market_address)
59
+
60
+ def collateral_token(self, web3: Web3 | None = None) -> ChecksumAddress:
61
+ collateral_token_address: str = self.call("collateralToken", web3=web3)
62
+ return Web3.to_checksum_address(collateral_token_address)
63
+
64
+ def create_categorical_market(
65
+ self,
66
+ api_keys: APIKeys,
67
+ params: CreateCategoricalMarketsParams,
68
+ web3: Web3 | None = None,
69
+ ) -> TxReceipt:
70
+ receipt_tx = self.send(
71
+ api_keys=api_keys,
72
+ function_name="createCategoricalMarket",
73
+ function_params=[params.model_dump(by_alias=True)],
74
+ web3=web3,
75
+ )
76
+ return receipt_tx
@@ -46,6 +46,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
46
46
  markets_field.factory,
47
47
  markets_field.creator,
48
48
  markets_field.marketName,
49
+ markets_field.parentOutcome,
49
50
  markets_field.outcomes,
50
51
  markets_field.parentMarket.id,
51
52
  markets_field.finalizeTs,
@@ -126,7 +127,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
126
127
  ]
127
128
  return fields
128
129
 
129
- def get_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
130
+ def get_swapr_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
130
131
  # We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
131
132
  wheres = []
132
133
  for wrapped_token in market.wrapped_tokens:
@@ -378,7 +378,7 @@ def monitor_market_outcome_bias(
378
378
 
379
379
  if len(df) > 0:
380
380
  st.altair_chart(
381
- alt.layer(open_chart, resolved_chart).interactive(), # type: ignore # Doesn't expect `LayerChart`, but `Chart`, yet it works.
381
+ alt.layer(open_chart, resolved_chart).interactive(),
382
382
  use_container_width=True,
383
383
  )
384
384
 
@@ -1,5 +1,5 @@
1
1
  import typing as t
2
- from datetime import date, datetime, timedelta
2
+ from datetime import datetime, timedelta
3
3
 
4
4
  import streamlit as st
5
5
 
@@ -105,13 +105,9 @@ def monitor_app(
105
105
  start_time: DatetimeUTC | None = (
106
106
  DatetimeUTC.from_datetime(
107
107
  datetime.combine(
108
- t.cast(
109
- # This will be always a date for us, so casting.
110
- date,
111
- st.date_input(
112
- "Start time",
113
- value=utcnow() - timedelta(weeks=settings.PAST_N_WEEKS),
114
- ),
108
+ st.date_input(
109
+ "Start time",
110
+ value=utcnow() - timedelta(weeks=settings.PAST_N_WEEKS),
115
111
  ),
116
112
  datetime.min.time(),
117
113
  )
@@ -22,11 +22,7 @@ from prediction_market_agent_tooling.gtypes import (
22
22
  )
23
23
  from prediction_market_agent_tooling.tools.data_models import MessageContainer
24
24
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
25
- from prediction_market_agent_tooling.tools.utils import (
26
- BPS_CONSTANT,
27
- DatetimeUTC,
28
- should_not_happen,
29
- )
25
+ from prediction_market_agent_tooling.tools.utils import BPS_CONSTANT, DatetimeUTC
30
26
  from prediction_market_agent_tooling.tools.web3_utils import (
31
27
  call_function_on_contract,
32
28
  send_function_on_contract_tx,
@@ -798,99 +794,6 @@ def init_collateral_token_contract(
798
794
  )
799
795
 
800
796
 
801
- def auto_deposit_collateral_token(
802
- collateral_token_contract: ContractERC20BaseClass,
803
- amount_wei: Wei,
804
- api_keys: APIKeys,
805
- web3: Web3 | None,
806
- ) -> None:
807
- if isinstance(collateral_token_contract, ContractERC4626BaseClass):
808
- auto_deposit_erc4626(collateral_token_contract, amount_wei, api_keys, web3)
809
-
810
- elif isinstance(
811
- collateral_token_contract, ContractDepositableWrapperERC20BaseClass
812
- ):
813
- auto_deposit_depositable_wrapper_erc20(
814
- collateral_token_contract, amount_wei, api_keys, web3
815
- )
816
-
817
- elif isinstance(collateral_token_contract, ContractERC20BaseClass):
818
- if (
819
- collateral_token_contract.balanceOf(
820
- for_address=api_keys.bet_from_address, web3=web3
821
- )
822
- < amount_wei
823
- ):
824
- raise ValueError(
825
- f"Not enough of the collateral token, but it's not a wrapper token that we can deposit automatically."
826
- )
827
-
828
- else:
829
- should_not_happen("Unsupported ERC20 contract type.")
830
-
831
-
832
- def auto_deposit_depositable_wrapper_erc20(
833
- collateral_token_contract: ContractDepositableWrapperERC20BaseClass,
834
- amount_wei: Wei,
835
- api_keys: APIKeys,
836
- web3: Web3 | None,
837
- ) -> None:
838
- collateral_token_balance = collateral_token_contract.balanceOf(
839
- for_address=api_keys.bet_from_address, web3=web3
840
- )
841
-
842
- # If we have enough of the collateral token, we don't need to deposit.
843
- if collateral_token_balance >= amount_wei:
844
- return
845
-
846
- # If we don't have enough, we need to deposit the difference.
847
- left_to_deposit = Wei(amount_wei - collateral_token_balance)
848
- collateral_token_contract.deposit(api_keys, left_to_deposit, web3=web3)
849
-
850
-
851
- def auto_deposit_erc4626(
852
- collateral_token_contract: ContractERC4626BaseClass,
853
- asset_amount_wei: Wei,
854
- api_keys: APIKeys,
855
- web3: Web3 | None,
856
- ) -> None:
857
- for_address = api_keys.bet_from_address
858
- collateral_token_balance_in_shares = collateral_token_contract.balanceOf(
859
- for_address=for_address, web3=web3
860
- )
861
- asset_amount_wei_in_shares = collateral_token_contract.convertToShares(
862
- asset_amount_wei, web3
863
- )
864
-
865
- # If we have enough shares, we don't need to deposit.
866
- if collateral_token_balance_in_shares >= asset_amount_wei_in_shares:
867
- return
868
-
869
- # If we need to deposit into erc4626, we first need to have enough of the asset token.
870
- asset_token_contract = collateral_token_contract.get_asset_token_contract(web3=web3)
871
-
872
- # If the asset token is Depositable Wrapper ERC-20, we can deposit it, in case we don't have enough.
873
- if (
874
- collateral_token_contract.get_asset_token_balance(for_address, web3)
875
- < asset_amount_wei
876
- ):
877
- if isinstance(asset_token_contract, ContractDepositableWrapperERC20BaseClass):
878
- auto_deposit_depositable_wrapper_erc20(
879
- asset_token_contract, asset_amount_wei, api_keys, web3
880
- )
881
- else:
882
- raise ValueError(
883
- "Not enough of the asset token, but it's not a depositable wrapper token that we can deposit automatically."
884
- )
885
-
886
- # Finally, we can deposit the asset token into the erc4626 vault.
887
- collateral_token_balance_in_assets = collateral_token_contract.convertToAssets(
888
- collateral_token_balance_in_shares, web3
889
- )
890
- left_to_deposit = Wei(asset_amount_wei - collateral_token_balance_in_assets)
891
- collateral_token_contract.deposit_asset_token(left_to_deposit, api_keys, web3)
892
-
893
-
894
797
  def to_gnosis_chain_contract(
895
798
  contract: ContractERC20BaseClass,
896
799
  ) -> ContractERC20OnGnosisChain:
@@ -0,0 +1,133 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+
4
+ import httpx
5
+ from cowdao_cowpy import swap_tokens
6
+ from cowdao_cowpy.common.chains import Chain
7
+ from cowdao_cowpy.common.config import SupportedChainId
8
+ from cowdao_cowpy.common.constants import CowContractAddress
9
+ from cowdao_cowpy.order_book.api import OrderBookApi
10
+ from cowdao_cowpy.order_book.config import Envs, OrderBookAPIConfigFactory
11
+ from cowdao_cowpy.order_book.generated.model import (
12
+ Address,
13
+ OrderMetaData,
14
+ OrderQuoteRequest,
15
+ OrderQuoteSide1,
16
+ OrderQuoteSideKindSell,
17
+ OrderStatus,
18
+ TokenAmount,
19
+ )
20
+ from eth_account.signers.local import LocalAccount
21
+ from eth_typing.evm import ChecksumAddress
22
+ from web3 import Web3
23
+ from web3.types import Wei
24
+
25
+ from prediction_market_agent_tooling.config import APIKeys
26
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei, wei_type
27
+ from prediction_market_agent_tooling.loggers import logger
28
+ from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
29
+ from prediction_market_agent_tooling.tools.utils import utcnow
30
+
31
+
32
+ def get_order_book_api(env: Envs, chain: Chain) -> OrderBookApi:
33
+ chain_id = SupportedChainId(chain.value[0])
34
+ return OrderBookApi(OrderBookAPIConfigFactory.get_config(env, chain_id))
35
+
36
+
37
+ def get_buy_token_amount(
38
+ amount_wei: Wei,
39
+ sell_token: ChecksumAddress,
40
+ buy_token: ChecksumAddress,
41
+ chain: Chain = Chain.GNOSIS,
42
+ env: Envs = "prod",
43
+ ) -> Wei:
44
+ order_book_api = get_order_book_api(env, chain)
45
+ order_quote_request = OrderQuoteRequest(
46
+ sellToken=Address(sell_token),
47
+ buyToken=Address(buy_token),
48
+ from_=Address(
49
+ "0x1234567890abcdef1234567890abcdef12345678"
50
+ ), # Just random address, doesn't matter.
51
+ )
52
+ order_side = OrderQuoteSide1(
53
+ kind=OrderQuoteSideKindSell.sell,
54
+ sellAmountBeforeFee=TokenAmount(str(amount_wei)),
55
+ )
56
+ order_quote = asyncio.run(
57
+ order_book_api.post_quote(order_quote_request, order_side)
58
+ )
59
+ return wei_type(order_quote.quote.buyAmount.root)
60
+
61
+
62
+ def swap_tokens_waiting(
63
+ amount_wei: Wei,
64
+ sell_token: ChecksumAddress,
65
+ buy_token: ChecksumAddress,
66
+ api_keys: APIKeys,
67
+ chain: Chain = Chain.GNOSIS,
68
+ env: Envs = "prod",
69
+ web3: Web3 | None = None,
70
+ ) -> OrderMetaData:
71
+ account = api_keys.get_account()
72
+
73
+ # Approve the CoW Swap Vault Relayer to get the sell token.
74
+ ContractERC20OnGnosisChain(address=sell_token).approve(
75
+ api_keys,
76
+ Web3.to_checksum_address(CowContractAddress.VAULT_RELAYER.value),
77
+ amount_wei=amount_wei,
78
+ web3=web3,
79
+ )
80
+
81
+ # CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
82
+ return asyncio.run(
83
+ swap_tokens_waiting_async(
84
+ amount_wei, sell_token, buy_token, account, chain, env
85
+ )
86
+ )
87
+
88
+
89
+ async def swap_tokens_waiting_async(
90
+ amount_wei: Wei,
91
+ sell_token: ChecksumAddress,
92
+ buy_token: ChecksumAddress,
93
+ account: LocalAccount,
94
+ chain: Chain,
95
+ env: Envs,
96
+ timeout: timedelta = timedelta(seconds=60),
97
+ ) -> OrderMetaData:
98
+ order = await swap_tokens(
99
+ amount=amount_wei,
100
+ sell_token=sell_token,
101
+ buy_token=buy_token,
102
+ account=account,
103
+ chain=chain,
104
+ env=env,
105
+ )
106
+ logger.info(f"Order created: {order}")
107
+ start_time = utcnow()
108
+
109
+ while True:
110
+ async with httpx.AsyncClient() as client:
111
+ response = await client.get(order.url)
112
+ order_metadata = OrderMetaData.model_validate(response.json())
113
+
114
+ if order_metadata.status == OrderStatus.fulfilled:
115
+ logger.info(f"Order {order.uid} ({order.url}) completed.")
116
+ return order_metadata
117
+
118
+ elif order_metadata.status in (
119
+ OrderStatus.cancelled,
120
+ OrderStatus.expired,
121
+ ):
122
+ raise ValueError(f"Order {order.uid} failed. {order.url}")
123
+
124
+ if utcnow() - start_time > timeout:
125
+ raise TimeoutError(
126
+ f"Timeout waiting for order {order.uid} to be completed. {order.url}"
127
+ )
128
+
129
+ logger.info(
130
+ f"Order status of {order.uid} ({order.url}): {order_metadata.status}, waiting..."
131
+ )
132
+
133
+ await asyncio.sleep(3.14)