prediction-market-agent-tooling 0.61.2.dev479__py3-none-any.whl → 0.62.0__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 (50) hide show
  1. prediction_market_agent_tooling/config.py +2 -3
  2. prediction_market_agent_tooling/deploy/agent.py +4 -5
  3. prediction_market_agent_tooling/deploy/betting_strategy.py +53 -69
  4. prediction_market_agent_tooling/gtypes.py +105 -27
  5. prediction_market_agent_tooling/jobs/jobs_models.py +5 -7
  6. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +13 -17
  7. prediction_market_agent_tooling/markets/agent_market.py +96 -53
  8. prediction_market_agent_tooling/markets/blockchain_utils.py +1 -27
  9. prediction_market_agent_tooling/markets/data_models.py +40 -44
  10. prediction_market_agent_tooling/markets/manifold/api.py +2 -6
  11. prediction_market_agent_tooling/markets/manifold/data_models.py +33 -25
  12. prediction_market_agent_tooling/markets/manifold/manifold.py +13 -11
  13. prediction_market_agent_tooling/markets/market_fees.py +6 -2
  14. prediction_market_agent_tooling/markets/omen/data_models.py +66 -57
  15. prediction_market_agent_tooling/markets/omen/omen.py +222 -250
  16. prediction_market_agent_tooling/markets/omen/omen_contracts.py +31 -53
  17. prediction_market_agent_tooling/markets/omen/omen_resolving.py +7 -14
  18. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +20 -14
  19. prediction_market_agent_tooling/markets/polymarket/data_models.py +3 -3
  20. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +4 -4
  21. prediction_market_agent_tooling/markets/polymarket/polymarket.py +3 -5
  22. prediction_market_agent_tooling/markets/seer/data_models.py +8 -8
  23. prediction_market_agent_tooling/markets/seer/seer.py +85 -71
  24. prediction_market_agent_tooling/markets/seer/seer_contracts.py +10 -5
  25. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +5 -2
  26. prediction_market_agent_tooling/monitor/monitor.py +2 -2
  27. prediction_market_agent_tooling/tools/_generic_value.py +261 -0
  28. prediction_market_agent_tooling/tools/balances.py +14 -11
  29. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +12 -10
  30. prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +31 -24
  31. prediction_market_agent_tooling/tools/betting_strategies/utils.py +3 -1
  32. prediction_market_agent_tooling/tools/contract.py +14 -10
  33. prediction_market_agent_tooling/tools/cow/cow_manager.py +3 -4
  34. prediction_market_agent_tooling/tools/cow/cow_order.py +51 -7
  35. prediction_market_agent_tooling/tools/langfuse_client_utils.py +13 -1
  36. prediction_market_agent_tooling/tools/omen/sell_positions.py +6 -3
  37. prediction_market_agent_tooling/tools/safe.py +5 -6
  38. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +36 -27
  39. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +4 -25
  40. prediction_market_agent_tooling/tools/tokens/main_token.py +2 -2
  41. prediction_market_agent_tooling/tools/tokens/token_utils.py +46 -0
  42. prediction_market_agent_tooling/tools/tokens/usd.py +79 -0
  43. prediction_market_agent_tooling/tools/utils.py +14 -8
  44. prediction_market_agent_tooling/tools/web3_utils.py +24 -41
  45. {prediction_market_agent_tooling-0.61.2.dev479.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/METADATA +2 -1
  46. {prediction_market_agent_tooling-0.61.2.dev479.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/RECORD +49 -47
  47. prediction_market_agent_tooling/abis/gvp2_settlement.abi.json +0 -89
  48. {prediction_market_agent_tooling-0.61.2.dev479.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/LICENSE +0 -0
  49. {prediction_market_agent_tooling-0.61.2.dev479.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/WHEEL +0 -0
  50. {prediction_market_agent_tooling-0.61.2.dev479.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/entry_points.txt +0 -0
@@ -4,15 +4,16 @@ from eth_typing import ChecksumAddress
4
4
  from web3 import Web3
5
5
  from web3.types import TxReceipt
6
6
 
7
- from prediction_market_agent_tooling.config import APIKeys
7
+ from prediction_market_agent_tooling.config import APIKeys, RPCConfig
8
8
  from prediction_market_agent_tooling.gtypes import (
9
+ USD,
10
+ CollateralToken,
9
11
  HexAddress,
10
12
  HexBytes,
11
13
  OutcomeStr,
12
- Wei,
13
- wei_type,
14
+ OutcomeToken,
15
+ OutcomeWei,
14
16
  xDai,
15
- xdai_type,
16
17
  )
17
18
  from prediction_market_agent_tooling.loggers import logger
18
19
  from prediction_market_agent_tooling.markets.agent_market import (
@@ -23,15 +24,9 @@ from prediction_market_agent_tooling.markets.agent_market import (
23
24
  SortBy,
24
25
  )
25
26
  from prediction_market_agent_tooling.markets.blockchain_utils import store_trades
26
- from prediction_market_agent_tooling.markets.data_models import (
27
- BetAmount,
28
- Currency,
29
- Position,
30
- TokenAmount,
31
- )
27
+ from prediction_market_agent_tooling.markets.data_models import ExistingPosition
32
28
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
33
29
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
34
- from prediction_market_agent_tooling.markets.omen.omen_contracts import sDaiContract
35
30
  from prediction_market_agent_tooling.markets.seer.data_models import (
36
31
  NewMarketEvent,
37
32
  SeerMarket,
@@ -43,7 +38,6 @@ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
43
38
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
44
39
  SeerSubgraphHandler,
45
40
  )
46
- from prediction_market_agent_tooling.tools.balances import get_balances
47
41
  from prediction_market_agent_tooling.tools.contract import (
48
42
  ContractERC20OnGnosisChain,
49
43
  init_collateral_token_contract,
@@ -57,14 +51,16 @@ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
57
51
  from prediction_market_agent_tooling.tools.tokens.auto_deposit import (
58
52
  auto_deposit_collateral_token,
59
53
  )
60
- from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai, xdai_to_wei
54
+ from prediction_market_agent_tooling.tools.tokens.usd import (
55
+ get_token_in_usd,
56
+ get_usd_in_token,
57
+ )
61
58
 
62
59
  # We place a larger bet amount by default than Omen so that cow presents valid quotes.
63
- SEER_TINY_BET_AMOUNT = xdai_type(0.1)
60
+ SEER_TINY_BET_AMOUNT = USD(0.1)
64
61
 
65
62
 
66
63
  class SeerAgentMarket(AgentMarket):
67
- currency = Currency.sDai
68
64
  wrapped_tokens: list[ChecksumAddress]
69
65
  creator: HexAddress
70
66
  collateral_token_contract_address_checksummed: ChecksumAddress
@@ -74,6 +70,16 @@ class SeerAgentMarket(AgentMarket):
74
70
  None # Seer markets don't have a description, so just default to None.
75
71
  )
76
72
 
73
+ def get_collateral_token_contract(
74
+ self, web3: Web3 | None = None
75
+ ) -> ContractERC20OnGnosisChain:
76
+ web3 = web3 or RPCConfig().get_web3()
77
+ return to_gnosis_chain_contract(
78
+ init_collateral_token_contract(
79
+ self.collateral_token_contract_address_checksummed, web3
80
+ )
81
+ )
82
+
77
83
  def store_prediction(
78
84
  self,
79
85
  processed_market: ProcessedMarket | None,
@@ -95,29 +101,28 @@ class SeerAgentMarket(AgentMarket):
95
101
  agent_name=agent_name,
96
102
  )
97
103
 
98
- def _convert_bet_amount_into_wei(self, bet_amount: BetAmount) -> Wei:
99
- if bet_amount.currency == self.currency:
100
- return xdai_to_wei(xdai_type(bet_amount.amount))
101
- raise ValueError(
102
- f"Currencies don't match. Currency bet amount {bet_amount.currency} currency market: {self.currency}"
103
- )
104
+ def get_token_in_usd(self, x: CollateralToken) -> USD:
105
+ return get_token_in_usd(x, self.collateral_token_contract_address_checksummed)
106
+
107
+ def get_usd_in_token(self, x: USD) -> CollateralToken:
108
+ return get_usd_in_token(x, self.collateral_token_contract_address_checksummed)
104
109
 
105
110
  def get_buy_token_amount(
106
- self, bet_amount: BetAmount, direction: bool
107
- ) -> TokenAmount:
111
+ self, bet_amount: USD | CollateralToken, direction: bool
112
+ ) -> OutcomeToken:
108
113
  """Returns number of outcome tokens returned for a given bet expressed in collateral units."""
109
114
 
110
115
  outcome_token = self.get_wrapped_token_for_outcome(direction)
111
-
112
- bet_amount_in_wei = self._convert_bet_amount_into_wei(bet_amount=bet_amount)
116
+ bet_amount_in_tokens = self.get_in_token(bet_amount)
117
+ bet_amount_in_wei = bet_amount_in_tokens.as_wei
113
118
 
114
119
  quote = CowManager().get_quote(
115
120
  buy_token=outcome_token,
116
121
  sell_amount=bet_amount_in_wei,
117
122
  collateral_token=self.collateral_token_contract_address_checksummed,
118
123
  )
119
- sell_amount = wei_to_xdai(wei_type(quote.quote.buyAmount.root))
120
- return TokenAmount(amount=sell_amount, currency=bet_amount.currency)
124
+ sell_amount = OutcomeWei(quote.quote.buyAmount.root).as_outcome_token
125
+ return sell_amount
121
126
 
122
127
  def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
123
128
  outcome_translated = SeerOutcomeEnum.from_bool(outcome)
@@ -125,33 +130,46 @@ class SeerAgentMarket(AgentMarket):
125
130
  return OutcomeStr(self.outcomes[idx])
126
131
 
127
132
  @staticmethod
128
- def get_trade_balance(api_keys: APIKeys) -> float:
133
+ def get_trade_balance(api_keys: APIKeys) -> USD:
129
134
  return OmenAgentMarket.get_trade_balance(api_keys=api_keys)
130
135
 
131
- @classmethod
132
- def get_tiny_bet_amount(cls) -> BetAmount:
133
- return BetAmount(amount=SEER_TINY_BET_AMOUNT, currency=cls.currency)
136
+ def get_tiny_bet_amount(self) -> CollateralToken:
137
+ return self.get_in_token(SEER_TINY_BET_AMOUNT)
134
138
 
135
- def get_position(self, user_id: str, web3: Web3 | None = None) -> Position | None:
139
+ def get_position(
140
+ self, user_id: str, web3: Web3 | None = None
141
+ ) -> ExistingPosition | None:
136
142
  """
137
143
  Fetches position from the user in a given market.
138
144
  We ignore the INVALID balances since we are only interested in binary outcomes.
139
145
  """
140
146
 
141
- amounts = {}
147
+ amounts_ot: dict[OutcomeStr, OutcomeToken] = {}
142
148
 
143
149
  for outcome in [True, False]:
144
150
  wrapped_token = self.get_wrapped_token_for_outcome(outcome)
145
151
 
146
- outcome_token_balance = ContractERC20OnGnosisChain(
147
- address=wrapped_token
148
- ).balanceOf(for_address=Web3.to_checksum_address(user_id), web3=web3)
149
- outcome_str = self.get_outcome_str_from_bool(outcome=outcome)
150
- amounts[outcome_str] = TokenAmount(
151
- amount=wei_to_xdai(outcome_token_balance), currency=self.currency
152
+ outcome_token_balance_wei = OutcomeWei.from_wei(
153
+ ContractERC20OnGnosisChain(address=wrapped_token).balanceOf(
154
+ for_address=Web3.to_checksum_address(user_id), web3=web3
155
+ )
152
156
  )
153
-
154
- return Position(market_id=self.id, amounts=amounts)
157
+ outcome_str = self.get_outcome_str_from_bool(outcome=outcome)
158
+ amounts_ot[outcome_str] = outcome_token_balance_wei.as_outcome_token
159
+
160
+ amounts_current = {
161
+ k: self.get_token_in_usd(self.get_sell_value_of_outcome_token(k, v))
162
+ for k, v in amounts_ot.items()
163
+ }
164
+ amounts_potential = {
165
+ k: self.get_token_in_usd(v.as_token) for k, v in amounts_ot.items()
166
+ }
167
+ return ExistingPosition(
168
+ market_id=self.id,
169
+ amounts_current=amounts_current,
170
+ amounts_potential=amounts_potential,
171
+ amounts_ot=amounts_ot,
172
+ )
155
173
 
156
174
  @staticmethod
157
175
  def get_user_id(api_keys: APIKeys) -> str:
@@ -210,9 +228,9 @@ class SeerAgentMarket(AgentMarket):
210
228
  CowManager().get_quote(
211
229
  collateral_token=self.collateral_token_contract_address_checksummed,
212
230
  buy_token=outcome_token,
213
- sell_amount=xdai_to_wei(
214
- xdai_type(1)
215
- ), # we take 1 xDai as a baseline value for common trades the agents take.
231
+ sell_amount=CollateralToken(
232
+ 1
233
+ ).as_wei, # we take 1 as a baseline value for common trades the agents take.
216
234
  )
217
235
  return True
218
236
  except NoLiquidityAvailableOnCowException:
@@ -236,7 +254,7 @@ class SeerAgentMarket(AgentMarket):
236
254
  def place_bet(
237
255
  self,
238
256
  outcome: bool,
239
- amount: BetAmount,
257
+ amount: USD,
240
258
  auto_deposit: bool = True,
241
259
  web3: Web3 | None = None,
242
260
  api_keys: APIKeys | None = None,
@@ -247,32 +265,27 @@ class SeerAgentMarket(AgentMarket):
247
265
  f"Market {self.id} is not open for trading. Cannot place bet."
248
266
  )
249
267
 
250
- if amount.currency != self.currency:
251
- raise ValueError(f"Seer bets are made in xDai. Got {amount.currency}.")
268
+ amount_in_token = self.get_usd_in_token(amount)
269
+ amount_wei = amount_in_token.as_wei
270
+ collateral_contract = self.get_collateral_token_contract()
252
271
 
253
- collateral_contract = sDaiContract()
254
272
  if auto_deposit:
255
- # We convert the deposit amount (in sDai) to assets in order to convert.
256
- asset_amount = collateral_contract.convertToAssets(
257
- xdai_to_wei(xdai_type(amount.amount))
258
- )
259
273
  auto_deposit_collateral_token(
260
- collateral_contract, asset_amount, api_keys, web3
274
+ collateral_contract, amount_wei, api_keys, web3
261
275
  )
262
276
 
263
- # We require that amount is given in sDAI.
264
- collateral_balance = get_balances(address=api_keys.bet_from_address, web3=web3)
265
- if collateral_balance.sdai < amount.amount:
277
+ collateral_balance = collateral_contract.balanceOf(api_keys.bet_from_address)
278
+ if collateral_balance < amount_wei:
266
279
  raise ValueError(
267
- f"Balance {collateral_balance.sdai} not enough for bet size {amount.amount}"
280
+ f"Balance {collateral_balance} not enough for bet size {amount}"
268
281
  )
269
282
 
270
283
  outcome_token = self.get_wrapped_token_for_outcome(outcome)
271
- # Sell sDAI using token address
284
+ # Sell using token address
272
285
  order_metadata = CowManager().swap(
273
- amount=xdai_type(amount.amount),
286
+ amount=amount_in_token,
274
287
  sell_token=collateral_contract.address,
275
- buy_token=Web3.to_checksum_address(outcome_token),
288
+ buy_token=outcome_token,
276
289
  api_keys=api_keys,
277
290
  web3=web3,
278
291
  )
@@ -282,18 +295,17 @@ class SeerAgentMarket(AgentMarket):
282
295
 
283
296
  def seer_create_market_tx(
284
297
  api_keys: APIKeys,
285
- initial_funds: xDai,
298
+ initial_funds: USD | CollateralToken,
286
299
  question: str,
287
300
  opening_time: DatetimeUTC,
288
301
  language: str,
289
- outcomes: list[str],
302
+ outcomes: t.Sequence[OutcomeStr],
290
303
  auto_deposit: bool,
291
304
  category: str,
292
305
  min_bond_xdai: xDai,
293
306
  web3: Web3 | None = None,
294
307
  ) -> ChecksumAddress:
295
308
  web3 = web3 or SeerMarketFactory.get_web3() # Default to Gnosis web3.
296
- initial_funds_wei = xdai_to_wei(initial_funds)
297
309
 
298
310
  factory_contract = SeerMarketFactory()
299
311
  collateral_token_address = factory_contract.collateral_token(web3=web3)
@@ -301,24 +313,26 @@ def seer_create_market_tx(
301
313
  init_collateral_token_contract(collateral_token_address, web3)
302
314
  )
303
315
 
316
+ initial_funds_in_collateral = (
317
+ get_usd_in_token(initial_funds, collateral_token_address)
318
+ if isinstance(initial_funds, USD)
319
+ else initial_funds
320
+ )
321
+ initial_funds_in_collateral_wei = initial_funds_in_collateral.as_wei
322
+
304
323
  if auto_deposit:
305
324
  auto_deposit_collateral_token(
306
325
  collateral_token_contract=collateral_token_contract,
307
326
  api_keys=api_keys,
308
- amount_wei=initial_funds_wei,
327
+ collateral_amount_wei_or_usd=initial_funds_in_collateral_wei,
309
328
  web3=web3,
310
329
  )
311
330
 
312
- # In case of ERC4626, obtained (for example) sDai out of xDai could be lower than the `amount_wei`, so we need to handle it.
313
- initial_funds_in_shares = collateral_token_contract.get_in_shares(
314
- amount=initial_funds_wei, web3=web3
315
- )
316
-
317
331
  # Approve the market maker to withdraw our collateral token.
318
332
  collateral_token_contract.approve(
319
333
  api_keys=api_keys,
320
334
  for_address=factory_contract.address,
321
- amount_wei=initial_funds_in_shares,
335
+ amount_wei=initial_funds_in_collateral_wei,
322
336
  web3=web3,
323
337
  )
324
338
 
@@ -329,7 +343,7 @@ def seer_create_market_tx(
329
343
  opening_time=opening_time,
330
344
  language=language,
331
345
  category=category,
332
- min_bond_xdai=min_bond_xdai,
346
+ min_bond=min_bond_xdai,
333
347
  )
334
348
  tx_receipt = factory_contract.create_categorical_market(
335
349
  api_keys=api_keys, params=params, web3=web3
@@ -1,10 +1,16 @@
1
1
  import os
2
+ import typing as t
2
3
 
3
4
  from web3 import Web3
4
5
  from web3.types import TxReceipt
5
6
 
6
7
  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.gtypes import (
9
+ ABI,
10
+ ChecksumAddress,
11
+ OutcomeStr,
12
+ xDai,
13
+ )
8
14
  from prediction_market_agent_tooling.markets.seer.data_models import (
9
15
  CreateCategoricalMarketsParams,
10
16
  )
@@ -13,7 +19,6 @@ from prediction_market_agent_tooling.tools.contract import (
13
19
  abi_field_validator,
14
20
  )
15
21
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
16
- from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei
17
22
 
18
23
 
19
24
  class SeerMarketFactory(ContractOnGnosisChain):
@@ -31,9 +36,9 @@ class SeerMarketFactory(ContractOnGnosisChain):
31
36
  @staticmethod
32
37
  def build_market_params(
33
38
  market_question: str,
34
- outcomes: list[str],
39
+ outcomes: t.Sequence[OutcomeStr],
35
40
  opening_time: DatetimeUTC,
36
- min_bond_xdai: xDai,
41
+ min_bond: xDai,
37
42
  language: str = "en_US",
38
43
  category: str = "misc",
39
44
  ) -> CreateCategoricalMarketsParams:
@@ -42,7 +47,7 @@ class SeerMarketFactory(ContractOnGnosisChain):
42
47
  token_names=[
43
48
  o.upper() for o in outcomes
44
49
  ], # Following usual token names on Seer (YES,NO).
45
- min_bond=xdai_to_wei(min_bond_xdai),
50
+ min_bond=min_bond.as_xdai_wei.value,
46
51
  opening_time=int(opening_time.timestamp()),
47
52
  outcomes=outcomes,
48
53
  lang=language,
@@ -15,6 +15,7 @@ from prediction_market_agent_tooling.markets.seer.data_models import (
15
15
  )
16
16
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
17
17
  from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
18
+ from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
18
19
 
19
20
 
20
21
  class SeerSubgraphHandler(BaseSubgraphHandler):
@@ -158,7 +159,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
158
159
  first=(
159
160
  limit if limit else sys.maxsize
160
161
  ), # if not limit, we fetch all possible markets,
161
- where=where_stms,
162
+ where=unwrap_generic_value(where_stms),
162
163
  **optional_params,
163
164
  )
164
165
  fields = self._get_fields_for_markets(markets_field)
@@ -219,7 +220,9 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
219
220
  {"token1": wrapped_token.lower()},
220
221
  ]
221
222
  )
222
- pools_field = self.swapr_algebra_subgraph.Query.pools(where={"or": wheres})
223
+ pools_field = self.swapr_algebra_subgraph.Query.pools(
224
+ where=unwrap_generic_value({"or": wheres})
225
+ )
223
226
  fields = self._get_fields_for_pools(pools_field)
224
227
  pools = self.do_query(fields=fields, pydantic_model=SeerPool)
225
228
  return pools
@@ -186,12 +186,12 @@ def monitor_agent(agent: DeployedAgent) -> None:
186
186
  return
187
187
  bets_info = {
188
188
  "Market Question": [bet.market_question for bet in agent_bets],
189
- "Bet Amount": [bet.amount.amount for bet in agent_bets],
189
+ "Bet Amount": [bet.amount for bet in agent_bets],
190
190
  "Bet Outcome": [bet.outcome for bet in agent_bets],
191
191
  "Created Time": [bet.created_time for bet in agent_bets],
192
192
  "Resolved Time": [bet.resolved_time for bet in agent_bets],
193
193
  "Is Correct": [bet.is_correct for bet in agent_bets],
194
- "Profit": [round(bet.profit.amount, 2) for bet in agent_bets],
194
+ "Profit": [round(bet.profit, 2) for bet in agent_bets],
195
195
  }
196
196
 
197
197
  # Time column to use for x-axes and sorting
@@ -0,0 +1,261 @@
1
+ import typing as t
2
+ from decimal import Decimal
3
+ from typing import TypeVar, overload
4
+
5
+ from pydantic import GetCoreSchemaHandler
6
+ from pydantic_core import CoreSchema, core_schema
7
+ from web3.types import Wei as WeiWeb3
8
+
9
+ InputValueType = TypeVar(
10
+ "InputValueType", bound=t.Union[str, int, float, WeiWeb3, Decimal]
11
+ )
12
+ InternalValueType = TypeVar("InternalValueType", bound=t.Union[int, float, WeiWeb3])
13
+
14
+
15
+ class _GenericValue(
16
+ t.Generic[InputValueType, InternalValueType],
17
+ # Not great, but it allows to serialize this object with plain json.
18
+ dict[t.Literal["value"] | t.Literal["type"], InternalValueType | str],
19
+ ):
20
+ """
21
+ A helper class intended for inheritance. Do not instantiate this class directly.
22
+
23
+ Example:
24
+
25
+ ```python
26
+ a = _GenericValue(10)
27
+ b = Token(100) # Token is a subclass of _GenericValue
28
+ c = xDai(100) # xDai is a subclass of _GenericValue
29
+ d = Mana(100) # Mana is a subclass of _GenericValue
30
+ e = xDai(50)
31
+
32
+ # Mypy will complain if we try to work with different currencies (types)
33
+ b - c # mypy will report incompatible types
34
+ c - d # mypy will report incompatible types
35
+ c - e # mypy will be ok
36
+ a - b # mypy won't report issues, as others are subclasses of _GenericValue, and that's a problem, so don't use _GenericValue directly
37
+
38
+ # Resulting types after arithmetic operations are as expected, so we don't need to wrap them as before (e.g. xdai_type(c + c))
39
+ x = c - e # x is of type xDai
40
+ x = c * e # x if of type xDai
41
+ x = c / e # x is of type float (pure value after division with same types)
42
+ x = c / 2 # x is of type xDai
43
+ x = c // 2 # x is of type xDai
44
+ x * x * 2 # x is of type xDai
45
+ ```
46
+
47
+ TODO: There are some type ignores which isn't cool, but it works and type-wise values are also correct. Idk how to explain it to mypy though.
48
+ """
49
+
50
+ GenericValueType = TypeVar(
51
+ "GenericValueType", bound="_GenericValue[InputValueType, InternalValueType]"
52
+ )
53
+
54
+ parser: t.Callable[[InputValueType], InternalValueType]
55
+
56
+ def __init_subclass__(
57
+ cls, parser: t.Callable[[InputValueType], InternalValueType]
58
+ ) -> None:
59
+ super().__init_subclass__()
60
+ cls.parser = parser
61
+
62
+ def __init__(self, value: InputValueType) -> None:
63
+ self.value: InternalValueType = self.parser(value)
64
+ super().__init__({"value": self.value, "type": self.__class__.__name__})
65
+
66
+ def __str__(self) -> str:
67
+ return f"{self.value}"
68
+
69
+ def __neg__(self: GenericValueType) -> GenericValueType:
70
+ return type(self)(-self.value) # type: ignore[arg-type]
71
+
72
+ def __abs__(self: GenericValueType) -> GenericValueType:
73
+ return type(self)(abs(self.value)) # type: ignore[arg-type]
74
+
75
+ def __sub__(
76
+ self: GenericValueType, other: GenericValueType | t.Literal[0]
77
+ ) -> GenericValueType:
78
+ if other == 0:
79
+ other = self.zero()
80
+ if not isinstance(other, _GenericValue):
81
+ raise TypeError("Cannot subtract different types")
82
+ if type(self) is not type(other):
83
+ raise TypeError("Cannot subtract different types")
84
+ return type(self)(self.value - other.value)
85
+
86
+ def __add__(
87
+ self: GenericValueType, other: GenericValueType | t.Literal[0]
88
+ ) -> GenericValueType:
89
+ if other == 0:
90
+ other = self.zero()
91
+ if not isinstance(other, _GenericValue):
92
+ raise TypeError("Cannot add different types")
93
+ if type(self) is not type(other):
94
+ raise TypeError("Cannot add different types")
95
+ return type(self)(self.value + other.value)
96
+
97
+ def __mul__(
98
+ self: GenericValueType, other: GenericValueType | int | float
99
+ ) -> GenericValueType:
100
+ if not isinstance(other, (_GenericValue, int, float)):
101
+ raise TypeError("Cannot multiply different types")
102
+ if not isinstance(other, (int, float)) and type(self) is not type(other):
103
+ raise TypeError("Cannot multiply different types")
104
+ return type(self)(self.value * (other if isinstance(other, (int, float)) else other.value)) # type: ignore
105
+
106
+ @overload
107
+ def __truediv__(self: GenericValueType, other: int | float) -> GenericValueType:
108
+ ...
109
+
110
+ @overload
111
+ def __truediv__(
112
+ self: GenericValueType, other: GenericValueType
113
+ ) -> InternalValueType:
114
+ ...
115
+
116
+ def __truediv__(
117
+ self: GenericValueType, other: GenericValueType | int | float
118
+ ) -> GenericValueType | InternalValueType:
119
+ if not isinstance(other, (_GenericValue, int, float)):
120
+ raise TypeError("Cannot multiply different types")
121
+ if not isinstance(other, (int, float)) and type(self) is not type(other):
122
+ raise TypeError("Cannot multiply different types")
123
+ if other == 0:
124
+ raise ZeroDivisionError("Cannot divide by zero")
125
+ if isinstance(other, (int, float)):
126
+ return type(self)(self.value / other) # type: ignore
127
+ else:
128
+ return self.value / other.value # type: ignore
129
+
130
+ @overload
131
+ def __floordiv__(self: GenericValueType, other: int | float) -> GenericValueType:
132
+ ...
133
+
134
+ @overload
135
+ def __floordiv__(
136
+ self: GenericValueType, other: GenericValueType
137
+ ) -> InternalValueType:
138
+ ...
139
+
140
+ def __floordiv__(
141
+ self: GenericValueType, other: GenericValueType | int | float
142
+ ) -> GenericValueType | InternalValueType:
143
+ if not isinstance(other, (_GenericValue, int, float)):
144
+ raise TypeError("Cannot multiply different types")
145
+ if not isinstance(other, (int, float)) and type(self) is not type(other):
146
+ raise TypeError("Cannot multiply different types")
147
+ if other == 0:
148
+ raise ZeroDivisionError("Cannot divide by zero")
149
+ if isinstance(other, (int, float)):
150
+ return type(self)(self.value // other) # type: ignore
151
+ else:
152
+ return self.value // other.value # type: ignore
153
+
154
+ def __lt__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool:
155
+ if other == 0:
156
+ other = self.zero()
157
+ if not isinstance(other, _GenericValue):
158
+ raise TypeError("Cannot compare different types")
159
+ if type(self) is not type(other):
160
+ raise TypeError("Cannot compare different types")
161
+ return bool(self.value < other.value)
162
+
163
+ def __le__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool:
164
+ if other == 0:
165
+ other = self.zero()
166
+ if not isinstance(other, _GenericValue):
167
+ raise TypeError("Cannot compare different types")
168
+ if type(self) is not type(other):
169
+ raise TypeError("Cannot compare different types")
170
+ return bool(self.value <= other.value)
171
+
172
+ def __gt__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool:
173
+ if other == 0:
174
+ other = self.zero()
175
+ if not isinstance(other, _GenericValue):
176
+ raise TypeError("Cannot compare different types")
177
+ if type(self) is not type(other):
178
+ raise TypeError("Cannot compare different types")
179
+ return bool(self.value > other.value)
180
+
181
+ def __ge__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool:
182
+ if other == 0:
183
+ other = self.zero()
184
+ if not isinstance(other, _GenericValue):
185
+ raise TypeError("Cannot compare different types")
186
+ if type(self) is not type(other):
187
+ raise TypeError("Cannot compare different types")
188
+ return bool(self.value >= other.value)
189
+
190
+ def __eq__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool: # type: ignore
191
+ if other == 0:
192
+ other = self.zero()
193
+ if not isinstance(other, _GenericValue):
194
+ raise TypeError("Cannot compare different types")
195
+ if type(self) is not type(other):
196
+ raise TypeError("Cannot compare different types")
197
+ return bool(self.value == other.value)
198
+
199
+ def __ne__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool: # type: ignore
200
+ if other == 0:
201
+ other = self.zero()
202
+ if not isinstance(other, _GenericValue):
203
+ raise TypeError("Cannot compare different types")
204
+ if type(self) is not type(other):
205
+ raise TypeError("Cannot compare different types")
206
+ return bool(self.value != other.value)
207
+
208
+ def __repr__(self) -> str:
209
+ return f"{type(self).__name__}({self.value})"
210
+
211
+ def __float__(self) -> float:
212
+ return float(self.value)
213
+
214
+ def __radd__(
215
+ self: GenericValueType, other: GenericValueType | t.Literal[0] | int | float
216
+ ) -> GenericValueType:
217
+ if isinstance(other, (_GenericValue, int, float)):
218
+ return self.__add__(other) # type: ignore[operator]
219
+
220
+ elif isinstance(other, (int, float)) and other == 0:
221
+ return self
222
+
223
+ else:
224
+ raise TypeError("Cannot add different types")
225
+
226
+ def __round__(self: GenericValueType, ndigits: int = 0) -> GenericValueType:
227
+ if not isinstance(self.value, (int, float)):
228
+ raise TypeError("Cannot round non-numeric types")
229
+ return type(self)(round(self.value, ndigits)) # type: ignore[arg-type]
230
+
231
+ def __bool__(self) -> bool:
232
+ return bool(self.value)
233
+
234
+ @classmethod
235
+ def __get_pydantic_core_schema__(
236
+ cls, source_type: t.Any, handler: GetCoreSchemaHandler
237
+ ) -> CoreSchema:
238
+ # Support for Pydantic usage.
239
+ dt_schema = handler(str | int | float | dict)
240
+ return core_schema.no_info_after_validator_function(
241
+ lambda x: cls(x["value"] if isinstance(x, dict) else x),
242
+ dt_schema,
243
+ )
244
+
245
+ def with_fraction(self: GenericValueType, fraction: float) -> GenericValueType:
246
+ if not 0 <= fraction <= 1:
247
+ raise ValueError(f"Given fraction {fraction} is not in the range [0,1].")
248
+ return self.__class__(self.value * (1 + fraction)) # type: ignore[arg-type]
249
+
250
+ def without_fraction(self: GenericValueType, fraction: float) -> GenericValueType:
251
+ if not 0 <= fraction <= 1:
252
+ raise ValueError(f"Given fraction {fraction} is not in the range [0,1].")
253
+ return self.__class__(self.value * (1 - fraction)) # type: ignore[arg-type]
254
+
255
+ @classmethod
256
+ def zero(cls: type[GenericValueType]) -> GenericValueType:
257
+ return cls(0) # type: ignore[arg-type]
258
+
259
+ @property
260
+ def symbol(self) -> str:
261
+ return self.__class__.__name__