prediction-market-agent-tooling 0.69.7.dev1114__py3-none-any.whl → 0.69.9__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.
@@ -0,0 +1,221 @@
1
+ [
2
+ {
3
+ "inputs": [
4
+ {
5
+ "internalType": "address",
6
+ "name": "_factory",
7
+ "type": "address"
8
+ },
9
+ {
10
+ "internalType": "address",
11
+ "name": "_WNativeToken",
12
+ "type": "address"
13
+ },
14
+ {
15
+ "internalType": "address",
16
+ "name": "_poolDeployer",
17
+ "type": "address"
18
+ }
19
+ ],
20
+ "stateMutability": "nonpayable",
21
+ "type": "constructor"
22
+ },
23
+ {
24
+ "inputs": [],
25
+ "name": "WNativeToken",
26
+ "outputs": [
27
+ {
28
+ "internalType": "address",
29
+ "name": "",
30
+ "type": "address"
31
+ }
32
+ ],
33
+ "stateMutability": "view",
34
+ "type": "function"
35
+ },
36
+ {
37
+ "inputs": [
38
+ {
39
+ "internalType": "int256",
40
+ "name": "amount0Delta",
41
+ "type": "int256"
42
+ },
43
+ {
44
+ "internalType": "int256",
45
+ "name": "amount1Delta",
46
+ "type": "int256"
47
+ },
48
+ {
49
+ "internalType": "bytes",
50
+ "name": "path",
51
+ "type": "bytes"
52
+ }
53
+ ],
54
+ "name": "algebraSwapCallback",
55
+ "outputs": [],
56
+ "stateMutability": "view",
57
+ "type": "function"
58
+ },
59
+ {
60
+ "inputs": [],
61
+ "name": "factory",
62
+ "outputs": [
63
+ {
64
+ "internalType": "address",
65
+ "name": "",
66
+ "type": "address"
67
+ }
68
+ ],
69
+ "stateMutability": "view",
70
+ "type": "function"
71
+ },
72
+ {
73
+ "inputs": [],
74
+ "name": "poolDeployer",
75
+ "outputs": [
76
+ {
77
+ "internalType": "address",
78
+ "name": "",
79
+ "type": "address"
80
+ }
81
+ ],
82
+ "stateMutability": "view",
83
+ "type": "function"
84
+ },
85
+ {
86
+ "inputs": [
87
+ {
88
+ "internalType": "bytes",
89
+ "name": "path",
90
+ "type": "bytes"
91
+ },
92
+ {
93
+ "internalType": "uint256",
94
+ "name": "amountIn",
95
+ "type": "uint256"
96
+ }
97
+ ],
98
+ "name": "quoteExactInput",
99
+ "outputs": [
100
+ {
101
+ "internalType": "uint256",
102
+ "name": "amountOut",
103
+ "type": "uint256"
104
+ },
105
+ {
106
+ "internalType": "uint16[]",
107
+ "name": "fees",
108
+ "type": "uint16[]"
109
+ }
110
+ ],
111
+ "stateMutability": "nonpayable",
112
+ "type": "function"
113
+ },
114
+ {
115
+ "inputs": [
116
+ {
117
+ "internalType": "address",
118
+ "name": "tokenIn",
119
+ "type": "address"
120
+ },
121
+ {
122
+ "internalType": "address",
123
+ "name": "tokenOut",
124
+ "type": "address"
125
+ },
126
+ {
127
+ "internalType": "uint256",
128
+ "name": "amountIn",
129
+ "type": "uint256"
130
+ },
131
+ {
132
+ "internalType": "uint160",
133
+ "name": "limitSqrtPrice",
134
+ "type": "uint160"
135
+ }
136
+ ],
137
+ "name": "quoteExactInputSingle",
138
+ "outputs": [
139
+ {
140
+ "internalType": "uint256",
141
+ "name": "amountOut",
142
+ "type": "uint256"
143
+ },
144
+ {
145
+ "internalType": "uint16",
146
+ "name": "fee",
147
+ "type": "uint16"
148
+ }
149
+ ],
150
+ "stateMutability": "nonpayable",
151
+ "type": "function"
152
+ },
153
+ {
154
+ "inputs": [
155
+ {
156
+ "internalType": "bytes",
157
+ "name": "path",
158
+ "type": "bytes"
159
+ },
160
+ {
161
+ "internalType": "uint256",
162
+ "name": "amountOut",
163
+ "type": "uint256"
164
+ }
165
+ ],
166
+ "name": "quoteExactOutput",
167
+ "outputs": [
168
+ {
169
+ "internalType": "uint256",
170
+ "name": "amountIn",
171
+ "type": "uint256"
172
+ },
173
+ {
174
+ "internalType": "uint16[]",
175
+ "name": "fees",
176
+ "type": "uint16[]"
177
+ }
178
+ ],
179
+ "stateMutability": "nonpayable",
180
+ "type": "function"
181
+ },
182
+ {
183
+ "inputs": [
184
+ {
185
+ "internalType": "address",
186
+ "name": "tokenIn",
187
+ "type": "address"
188
+ },
189
+ {
190
+ "internalType": "address",
191
+ "name": "tokenOut",
192
+ "type": "address"
193
+ },
194
+ {
195
+ "internalType": "uint256",
196
+ "name": "amountOut",
197
+ "type": "uint256"
198
+ },
199
+ {
200
+ "internalType": "uint160",
201
+ "name": "limitSqrtPrice",
202
+ "type": "uint160"
203
+ }
204
+ ],
205
+ "name": "quoteExactOutputSingle",
206
+ "outputs": [
207
+ {
208
+ "internalType": "uint256",
209
+ "name": "amountIn",
210
+ "type": "uint256"
211
+ },
212
+ {
213
+ "internalType": "uint16",
214
+ "name": "fee",
215
+ "type": "uint16"
216
+ }
217
+ ],
218
+ "stateMutability": "nonpayable",
219
+ "type": "function"
220
+ }
221
+ ]
@@ -179,6 +179,17 @@ class ExactInputSingleParams(BaseModel):
179
179
  ) # 0 for convenience, we also don't expect major price shifts
180
180
 
181
181
 
182
+ class QuoteExactInputSingleParams(BaseModel):
183
+ # from https://gnosisscan.io/address/0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7#writeContract
184
+ model_config = ConfigDict(populate_by_name=True)
185
+ token_in: ChecksumAddress = Field(alias="tokenIn")
186
+ token_out: ChecksumAddress = Field(alias="tokenOut")
187
+ amount_in: Wei = Field(alias="amountIn")
188
+ limit_sqrt_price: Wei = Field(
189
+ alias="limitSqrtPrice", default_factory=lambda: Wei(0)
190
+ ) # 0 for convenience, we also don't expect major price shifts
191
+
192
+
182
193
  class SeerTransactionType(str, Enum):
183
194
  SWAP = "swap"
184
195
  SPLIT = "split"
@@ -10,12 +10,17 @@ from prediction_market_agent_tooling.gtypes import (
10
10
  OutcomeStr,
11
11
  OutcomeToken,
12
12
  Probability,
13
+ Wei,
13
14
  )
14
15
  from prediction_market_agent_tooling.loggers import logger
15
16
  from prediction_market_agent_tooling.markets.seer.data_models import SeerMarket
16
17
  from prediction_market_agent_tooling.markets.seer.exceptions import (
17
18
  PriceCalculationError,
18
19
  )
20
+ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
21
+ QuoteExactInputSingleParams,
22
+ SwaprQuoterContract,
23
+ )
19
24
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
20
25
  SeerSubgraphHandler,
21
26
  )
@@ -52,16 +57,16 @@ class PriceManager:
52
57
  )
53
58
 
54
59
  def get_price_for_token(self, token: ChecksumAddress) -> CollateralToken | None:
55
- return self.get_amount_of_token_in_collateral(token, CollateralToken(1))
60
+ return self.get_amount_of_token_in_collateral(token, OutcomeToken(1))
56
61
 
57
62
  @cached(TTLCache(maxsize=100, ttl=5 * 60))
58
63
  def get_amount_of_collateral_in_token(
59
64
  self,
60
65
  token: ChecksumAddress,
61
66
  collateral_exchange_amount: CollateralToken,
62
- ) -> CollateralToken | None:
67
+ ) -> OutcomeToken | None:
63
68
  if token == self.seer_market.collateral_token_contract_address_checksummed:
64
- return collateral_exchange_amount
69
+ return OutcomeToken(collateral_exchange_amount.value)
65
70
 
66
71
  try:
67
72
  buy_token_amount = get_buy_token_amount_else_raise(
@@ -69,31 +74,31 @@ class PriceManager:
69
74
  sell_token=self.seer_market.collateral_token_contract_address_checksummed,
70
75
  buy_token=token,
71
76
  )
72
- return buy_token_amount.as_token
77
+ return OutcomeToken.from_token(buy_token_amount.as_token)
73
78
 
74
79
  except Exception as e:
75
80
  logger.warning(
76
81
  f"Could not get quote for {token=} from Cow, exception {e=}. Falling back to pools. "
77
82
  )
78
- prices = self.get_token_price_from_pools(token=token)
79
- return (
80
- prices.priceOfCollateralInAskingToken * collateral_exchange_amount
81
- if prices
82
- else None
83
+ quote = self.get_swapr_input_quote(
84
+ input_token=self.seer_market.collateral_token_contract_address_checksummed,
85
+ output_token=token,
86
+ input_amount=collateral_exchange_amount.as_wei,
83
87
  )
88
+ return OutcomeToken.from_token(quote.as_token)
84
89
 
85
90
  @cached(TTLCache(maxsize=100, ttl=5 * 60))
86
91
  def get_amount_of_token_in_collateral(
87
92
  self,
88
93
  token: ChecksumAddress,
89
- token_exchange_amount: CollateralToken,
94
+ token_exchange_amount: OutcomeToken,
90
95
  ) -> CollateralToken | None:
91
96
  if token == self.seer_market.collateral_token_contract_address_checksummed:
92
- return token_exchange_amount
97
+ return token_exchange_amount.as_token
93
98
 
94
99
  try:
95
100
  buy_collateral_amount = get_buy_token_amount_else_raise(
96
- sell_amount=token_exchange_amount.as_wei,
101
+ sell_amount=token_exchange_amount.as_outcome_wei.as_wei,
97
102
  sell_token=token,
98
103
  buy_token=self.seer_market.collateral_token_contract_address_checksummed,
99
104
  )
@@ -103,17 +108,21 @@ class PriceManager:
103
108
  logger.warning(
104
109
  f"Could not get quote for {token=} from Cow, exception {e=}. Falling back to pools. "
105
110
  )
106
- prices = self.get_token_price_from_pools(token=token)
107
- return (
108
- prices.priceOfAskingTokenInCollateral * token_exchange_amount
109
- if prices
110
- else None
111
+ quote = self.get_swapr_input_quote(
112
+ input_token=token,
113
+ output_token=self.seer_market.collateral_token_contract_address_checksummed,
114
+ input_amount=token_exchange_amount.as_outcome_wei.as_wei,
111
115
  )
116
+ return quote.as_token
112
117
 
113
118
  def get_token_price_from_pools(
114
119
  self,
115
120
  token: ChecksumAddress,
116
121
  ) -> Prices | None:
122
+ """
123
+ Although this might come handy,
124
+ consider using `get_amount_of_collateral_in_token` or `get_amount_of_token_in_collateral` to have an exact quote.
125
+ """
117
126
  pool = SeerSubgraphHandler().get_pool_by_token(
118
127
  token_address=token,
119
128
  collateral_address=self.seer_market.collateral_token_contract_address_checksummed,
@@ -248,3 +257,21 @@ class PriceManager:
248
257
  else:
249
258
  probability_map[outcome] = Probability(0)
250
259
  return probability_map, outcome_token_pool
260
+
261
+ def get_swapr_input_quote(
262
+ self,
263
+ input_token: ChecksumAddress,
264
+ output_token: ChecksumAddress,
265
+ input_amount: Wei,
266
+ web3: Web3 | None = None,
267
+ ) -> Wei: # Not marked as OutcomeWei, but this works for both buying and selling.
268
+ quoter = SwaprQuoterContract()
269
+ amount_out, _ = quoter.quote_exact_input_single(
270
+ QuoteExactInputSingleParams(
271
+ token_in=input_token,
272
+ token_out=output_token,
273
+ amount_in=input_amount,
274
+ ),
275
+ web3=web3,
276
+ )
277
+ return amount_out
@@ -159,6 +159,8 @@ class SeerAgentMarket(AgentMarket):
159
159
 
160
160
  def get_token_in_usd(self, x: CollateralToken) -> USD:
161
161
  p = self.get_price_manager()
162
+ # This function is meant to convert market's collateral token into usd value, however, on Seer, market's collateral can be another's market outcome token.
163
+ # That's why we need this middle step.
162
164
  sdai_amount = p.get_amount_of_collateral_in_token(
163
165
  # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
164
166
  SDAI_CONTRACT_ADDRESS,
@@ -168,14 +170,16 @@ class SeerAgentMarket(AgentMarket):
168
170
  raise RuntimeError(
169
171
  "Both CoW and pool-fallback way of getting price failed."
170
172
  )
171
- return get_token_in_usd(sdai_amount, SDAI_CONTRACT_ADDRESS)
173
+ return get_token_in_usd(sdai_amount.as_token, SDAI_CONTRACT_ADDRESS)
172
174
 
173
175
  def get_usd_in_token(self, x: USD) -> CollateralToken:
174
176
  p = self.get_price_manager()
177
+ # This function is meant to convert market's collateral token into usd value, however, on Seer, market's collateral can be another's market outcome token.
178
+ # That's why we need this middle step.
175
179
  token_amount = p.get_amount_of_token_in_collateral(
176
180
  # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
177
181
  SDAI_CONTRACT_ADDRESS,
178
- get_usd_in_token(x, SDAI_CONTRACT_ADDRESS),
182
+ OutcomeToken.from_token(get_usd_in_token(x, SDAI_CONTRACT_ADDRESS)),
179
183
  )
180
184
  if token_amount is None:
181
185
  raise RuntimeError(
@@ -205,7 +209,7 @@ class SeerAgentMarket(AgentMarket):
205
209
  logger.info(f"Could not get price for token {outcome_token}")
206
210
  return None
207
211
 
208
- return OutcomeToken(amount_outcome_tokens.value)
212
+ return amount_outcome_tokens
209
213
 
210
214
  def get_sell_value_of_outcome_token(
211
215
  self, outcome: OutcomeStr, amount: OutcomeToken
@@ -217,7 +221,7 @@ class SeerAgentMarket(AgentMarket):
217
221
 
218
222
  p = self.get_price_manager()
219
223
  value_outcome_token_in_collateral = p.get_amount_of_token_in_collateral(
220
- wrapped_outcome_token, amount.as_token
224
+ wrapped_outcome_token, amount
221
225
  )
222
226
 
223
227
  if value_outcome_token_in_collateral is None:
@@ -583,7 +587,7 @@ class SeerAgentMarket(AgentMarket):
583
587
  web3=web3,
584
588
  )
585
589
  collateral_balance = p.get_amount_of_token_in_collateral(
586
- token_address_checksummed, token_balance
590
+ token_address_checksummed, OutcomeToken.from_token(token_balance)
587
591
  )
588
592
 
589
593
  # We ignore the liquidity in outcome tokens if price unknown.
@@ -688,7 +692,7 @@ class SeerAgentMarket(AgentMarket):
688
692
  ).buy_or_sell_outcome_token(
689
693
  token_in=sell_token,
690
694
  token_out=buy_token,
691
- amount_wei=amount_wei,
695
+ amount_in=amount_wei,
692
696
  web3=web3,
693
697
  )
694
698
  swap_pool_tx_hash = tx_receipt["transactionHash"]
@@ -15,6 +15,7 @@ from prediction_market_agent_tooling.gtypes import (
15
15
  )
16
16
  from prediction_market_agent_tooling.markets.seer.data_models import (
17
17
  ExactInputSingleParams,
18
+ QuoteExactInputSingleParams,
18
19
  )
19
20
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
20
21
  CreateCategoricalMarketsParams,
@@ -191,3 +192,31 @@ class SwaprRouterContract(ContractOnGnosisChain):
191
192
  # Typical Swapr swaps use 150k-300k gas, we set conservative
192
193
  default_gas=400_000,
193
194
  )
195
+
196
+
197
+ class SwaprQuoterContract(ContractOnGnosisChain):
198
+ # File content taken from https://gnosisscan.io/address/0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7#code.
199
+ abi: ABI = abi_field_validator(
200
+ os.path.join(
201
+ os.path.dirname(os.path.realpath(__file__)),
202
+ "../../abis/swapr_quoter.abi.json",
203
+ )
204
+ )
205
+
206
+ address: ChecksumAddress = Web3.to_checksum_address(
207
+ "0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7"
208
+ )
209
+
210
+ def quote_exact_input_single(
211
+ self,
212
+ params: QuoteExactInputSingleParams,
213
+ web3: Web3 | None = None,
214
+ ) -> tuple[Wei, Wei]:
215
+ # See https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps.
216
+ result: tuple[int, int] = self.call(
217
+ function_name="quoteExactInputSingle",
218
+ function_params=list(dict(params).values()),
219
+ web3=web3,
220
+ )
221
+ amount_out, fee = result
222
+ return Wei(amount_out), Wei(fee)
@@ -3,7 +3,6 @@ from web3 import Web3
3
3
  from prediction_market_agent_tooling.config import APIKeys
4
4
  from prediction_market_agent_tooling.gtypes import (
5
5
  ChecksumAddress,
6
- CollateralToken,
7
6
  HexBytes,
8
7
  HexStr,
9
8
  TxReceipt,
@@ -36,22 +35,20 @@ class SwapPoolHandler:
36
35
 
37
36
  def _calculate_amount_out_minimum(
38
37
  self,
39
- amount_wei: Wei,
38
+ amount_in: Wei,
40
39
  token_in: ChecksumAddress,
41
- price_outcome_token: CollateralToken,
40
+ token_out: ChecksumAddress,
42
41
  buffer_pct: float = 0.05,
43
42
  ) -> Wei:
44
- is_buying_outcome = token_in == self.collateral_token_address
45
-
46
- if is_buying_outcome:
47
- value = amount_wei.value * (1.0 - buffer_pct) / price_outcome_token.value
48
- else:
49
- value = amount_wei.value * price_outcome_token.value * (1.0 - buffer_pct)
50
- return Wei(int(value))
43
+ price_manager = PriceManager.build(HexBytes(HexStr(self.market_id)))
44
+ value = price_manager.get_swapr_input_quote(
45
+ input_amount=amount_in, input_token=token_in, output_token=token_out
46
+ )
47
+ return value * (1 - buffer_pct)
51
48
 
52
49
  def buy_or_sell_outcome_token(
53
50
  self,
54
- amount_wei: Wei,
51
+ amount_in: Wei,
55
52
  token_in: ChecksumAddress,
56
53
  token_out: ChecksumAddress,
57
54
  web3: Web3 | None = None,
@@ -62,38 +59,22 @@ class SwapPoolHandler:
62
59
  f"trading outcome_token for a token different than collateral_token {self.collateral_token_address} is not supported. {token_in=} {token_out=}"
63
60
  )
64
61
 
65
- outcome_token = (
66
- token_in if token_in != self.collateral_token_address else token_out
67
- )
68
-
69
- # We could use a quoter contract (https://github.com/SwaprHQ/swapr-sdk/blob/develop/src/entities/trades/swapr-v3/constants.ts#L7), but since there is normally 1 pool per outcome token/collateral pair, it's not necessary.
70
-
71
- price_outcome_token = PriceManager.build(
72
- HexBytes(HexStr(self.market_id))
73
- ).get_token_price_from_pools(token=outcome_token)
74
- if (
75
- not price_outcome_token
76
- or not price_outcome_token.priceOfCollateralInAskingToken
77
- ):
78
- raise ValueError(
79
- f"Could not find price for {outcome_token=} and {self.collateral_token_address}"
80
- )
81
-
82
62
  amount_out_minimum = self._calculate_amount_out_minimum(
83
- amount_wei=amount_wei,
63
+ amount_in=amount_in,
84
64
  token_in=token_in,
85
- price_outcome_token=price_outcome_token.priceOfCollateralInAskingToken,
65
+ token_out=token_out,
86
66
  )
87
67
 
88
68
  p = ExactInputSingleParams(
89
69
  token_in=token_in,
90
70
  token_out=token_out,
91
71
  recipient=self.api_keys.bet_from_address,
92
- amount_in=amount_wei,
72
+ amount_in=amount_in,
93
73
  amount_out_minimum=amount_out_minimum,
94
74
  )
95
75
 
96
76
  tx_receipt = SwaprRouterContract().exact_input_single(
97
77
  api_keys=self.api_keys, params=p, web3=web3
98
78
  )
79
+
99
80
  return tx_receipt
@@ -1,6 +1,8 @@
1
+ import asyncio
1
2
  import hashlib
2
3
  import inspect
3
4
  import json
5
+ from dataclasses import dataclass
4
6
  from datetime import timedelta
5
7
  from functools import wraps
6
8
  from types import UnionType
@@ -12,6 +14,7 @@ from typing import (
12
14
  cast,
13
15
  get_args,
14
16
  get_origin,
17
+ get_type_hints,
15
18
  overload,
16
19
  )
17
20
 
@@ -101,136 +104,235 @@ def db_cache(
101
104
 
102
105
  api_keys = api_keys if api_keys is not None else APIKeys()
103
106
 
104
- @wraps(func)
105
- def wrapper(*args: Any, **kwargs: Any) -> Any:
106
- # If caching is disabled, just call the function and return it
107
- if not api_keys.ENABLE_CACHE:
108
- return func(*args, **kwargs)
107
+ # Check if the decorated function is async
108
+ if inspect.iscoroutinefunction(func):
109
109
 
110
- DBManager(api_keys.sqlalchemy_db_url.get_secret_value()).create_tables(
111
- [FunctionCache]
112
- )
110
+ @wraps(func)
111
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
112
+ # If caching is disabled, just call the function and return it
113
+ if not api_keys.ENABLE_CACHE:
114
+ return await func(*args, **kwargs)
113
115
 
114
- # Convert *args and **kwargs to a single dictionary, where we have names for arguments passed as args as well.
115
- signature = inspect.signature(func)
116
- bound_arguments = signature.bind(*args, **kwargs)
117
- bound_arguments.apply_defaults()
118
-
119
- # Convert any argument that is Pydantic model into classic dictionary, otherwise it won't be json-serializable.
120
- args_dict: dict[str, Any] = bound_arguments.arguments
121
-
122
- # Remove `self` or `cls` if present (in case of class' methods)
123
- if "self" in args_dict:
124
- del args_dict["self"]
125
- if "cls" in args_dict:
126
- del args_dict["cls"]
127
-
128
- # Remove ignored arguments
129
- if ignore_args:
130
- for arg in ignore_args:
131
- if arg in args_dict:
132
- del args_dict[arg]
133
-
134
- # Remove arguments of ignored types
135
- if ignore_arg_types:
136
- args_dict = {
137
- k: v
138
- for k, v in args_dict.items()
139
- if not isinstance(v, tuple(ignore_arg_types))
140
- }
116
+ # Run blocking database operations in thread pool
141
117
 
142
- # Compute a hash of the function arguments used for lookup of cached results
143
- arg_string = json.dumps(args_dict, sort_keys=True, default=str)
144
- args_hash = hashlib.md5(arg_string.encode()).hexdigest()
118
+ # Ensure tables in thread pool
119
+ await asyncio.to_thread(_ensure_tables, api_keys)
145
120
 
146
- # Get the full function name as concat of module and qualname, to not accidentally clash
147
- full_function_name = func.__module__ + "." + func.__qualname__
148
- # But also get the standard function name to easily search for it in database
149
- function_name = func.__name__
121
+ ctx = _build_context(func, args, kwargs, ignore_args, ignore_arg_types)
150
122
 
151
- # Determine if the function returns or contains Pydantic BaseModel(s)
152
- return_type = func.__annotations__.get("return", None)
153
- is_pydantic_model = return_type is not None and contains_pydantic_model(
154
- return_type
155
- )
123
+ # Fetch cached result in thread pool
124
+ lookup = await asyncio.to_thread(_fetch_cached, api_keys, ctx, max_age)
156
125
 
157
- with DBManager(
158
- api_keys.sqlalchemy_db_url.get_secret_value()
159
- ).get_session() as session:
160
- # Try to get cached result
161
- statement = (
162
- select(FunctionCache)
163
- .where(
164
- FunctionCache.function_name == function_name,
165
- FunctionCache.full_function_name == full_function_name,
166
- FunctionCache.args_hash == args_hash,
126
+ if lookup.hit:
127
+ logger.debug(
128
+ f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {ctx.full_function_name}"
167
129
  )
168
- .order_by(desc(FunctionCache.created_at))
169
- )
170
- if max_age is not None:
171
- cutoff_time = utcnow() - max_age
172
- statement = statement.where(FunctionCache.created_at >= cutoff_time)
173
- cached_result = session.exec(statement).first()
174
-
175
- if cached_result:
176
- logger.info(
177
- # Keep the special [case-hit] identifier so we can easily track it in GCP.
178
- f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {full_function_name} with args {args_dict} and output {cached_result.result}"
130
+ return lookup.value
131
+
132
+ computed_result = await func(*args, **kwargs)
133
+ logger.debug(
134
+ f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {ctx.full_function_name}"
179
135
  )
180
- if is_pydantic_model:
181
- # If the output contains any Pydantic models, we need to initialise them.
182
- try:
183
- return convert_cached_output_to_pydantic(
184
- return_type, cached_result.result
185
- )
186
- except ValueError as e:
187
- # In case of backward-incompatible pydantic model, just treat it as cache miss, to not error out.
188
- logger.warning(
189
- f"{DB_CACHE_LOG_PREFIX} [cache-miss] Can not validate {cached_result=} into {return_type=} because {e=}, treating as cache miss."
136
+
137
+ if cache_none or computed_result is not None:
138
+ # Save cached result in thread pool (fire-and-forget)
139
+ asyncio.create_task(
140
+ asyncio.to_thread(
141
+ _save_cached,
142
+ api_keys,
143
+ ctx,
144
+ computed_result,
145
+ log_error_on_unsavable_data,
190
146
  )
191
- cached_result = None
192
- else:
193
- return cached_result.result
147
+ )
148
+
149
+ return computed_result
150
+
151
+ return cast(FunctionT, async_wrapper)
152
+
153
+ @wraps(func)
154
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
155
+ if not api_keys.ENABLE_CACHE:
156
+ return func(*args, **kwargs)
157
+
158
+ _ensure_tables(api_keys)
159
+
160
+ ctx = _build_context(func, args, kwargs, ignore_args, ignore_arg_types)
161
+ lookup = _fetch_cached(api_keys, ctx, max_age)
162
+
163
+ if lookup.hit:
164
+ logger.debug(
165
+ f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {ctx.full_function_name}"
166
+ )
167
+ return lookup.value
194
168
 
195
- # On cache miss, compute the result
196
169
  computed_result = func(*args, **kwargs)
197
- # Keep the special [case-miss] identifier so we can easily track it in GCP.
198
- logger.info(
199
- f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {full_function_name} with args {args_dict}, computed the output {computed_result}"
170
+ logger.debug(
171
+ f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {ctx.full_function_name}"
200
172
  )
201
173
 
202
- # If postgres access was specified, save it.
203
174
  if cache_none or computed_result is not None:
204
- cache_entry = FunctionCache(
205
- function_name=function_name,
206
- full_function_name=full_function_name,
207
- args_hash=args_hash,
208
- args=args_dict,
209
- result=computed_result,
210
- created_at=utcnow(),
211
- )
212
- # Do not raise an exception if saving to the database fails, just log it and let the agent continue the work.
213
- try:
214
- with DBManager(
215
- api_keys.sqlalchemy_db_url.get_secret_value()
216
- ).get_session() as session:
217
- logger.info(
218
- f"{DB_CACHE_LOG_PREFIX} [cache-info] Saving {cache_entry} into database."
219
- )
220
- session.add(cache_entry)
221
- session.commit()
222
- except (DataError, psycopg2.errors.UntranslatableCharacter) as e:
223
- (logger.error if log_error_on_unsavable_data else logger.warning)(
224
- f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save {cache_entry} into database, ignoring, because: {e}"
225
- )
226
- except Exception:
227
- logger.exception(
228
- f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save {cache_entry} into database, ignoring."
229
- )
175
+ _save_cached(api_keys, ctx, computed_result, log_error_on_unsavable_data)
230
176
 
231
177
  return computed_result
232
178
 
233
- return cast(FunctionT, wrapper)
179
+ return cast(FunctionT, sync_wrapper)
180
+
181
+
182
+ @dataclass
183
+ class CallContext:
184
+ args_dict: dict[str, Any]
185
+ args_hash: str
186
+ function_name: str
187
+ full_function_name: str
188
+ return_type: Any
189
+
190
+ @property
191
+ def is_pydantic_model(self) -> bool:
192
+ return self.return_type is not None and contains_pydantic_model(
193
+ self.return_type
194
+ )
195
+
196
+
197
+ @dataclass
198
+ class CacheLookup:
199
+ hit: bool
200
+ value: Any | None = None
201
+
202
+
203
+ def _ensure_tables(api_keys: APIKeys) -> None:
204
+ DBManager(api_keys.sqlalchemy_db_url.get_secret_value()).create_tables(
205
+ [FunctionCache]
206
+ )
207
+
208
+
209
+ def _build_context(
210
+ func: Callable[..., Any],
211
+ args: tuple[Any, ...],
212
+ kwargs: dict[str, Any],
213
+ ignore_args: Sequence[str] | None,
214
+ ignore_arg_types: Sequence[type] | None,
215
+ ) -> CallContext:
216
+ signature = inspect.signature(func)
217
+ bound_arguments = signature.bind(*args, **kwargs)
218
+ bound_arguments.apply_defaults()
219
+
220
+ args_dict: dict[str, Any] = bound_arguments.arguments
221
+
222
+ if "self" in args_dict:
223
+ del args_dict["self"]
224
+ if "cls" in args_dict:
225
+ del args_dict["cls"]
226
+
227
+ if ignore_args:
228
+ for arg in ignore_args:
229
+ if arg in args_dict:
230
+ del args_dict[arg]
231
+
232
+ if ignore_arg_types:
233
+ args_dict = {
234
+ k: v
235
+ for k, v in args_dict.items()
236
+ if not isinstance(v, tuple(ignore_arg_types))
237
+ }
238
+
239
+ arg_string = json.dumps(args_dict, sort_keys=True, default=str)
240
+ args_hash = hashlib.md5(arg_string.encode()).hexdigest()
241
+
242
+ full_function_name = func.__module__ + "." + func.__qualname__
243
+ function_name = func.__name__
244
+
245
+ # Use get_type_hints to resolve forward references instead of __annotations__
246
+ try:
247
+ type_hints = get_type_hints(func)
248
+ return_type = type_hints.get("return", None)
249
+ except (NameError, AttributeError, TypeError) as e:
250
+ # Fallback to raw annotations if get_type_hints fails
251
+ logger.debug(
252
+ f"{DB_CACHE_LOG_PREFIX} Failed to resolve type hints for {full_function_name}, falling back to raw annotations: {e}"
253
+ )
254
+ return_type = func.__annotations__.get("return", None)
255
+
256
+ return CallContext(
257
+ args_dict=args_dict,
258
+ args_hash=args_hash,
259
+ function_name=function_name,
260
+ full_function_name=full_function_name,
261
+ return_type=return_type,
262
+ )
263
+
264
+
265
+ def _fetch_cached(
266
+ api_keys: APIKeys,
267
+ ctx: CallContext,
268
+ max_age: timedelta | None,
269
+ ) -> CacheLookup:
270
+ with DBManager(
271
+ api_keys.sqlalchemy_db_url.get_secret_value()
272
+ ).get_session() as session:
273
+ statement = (
274
+ select(FunctionCache)
275
+ .where(
276
+ FunctionCache.function_name == ctx.function_name,
277
+ FunctionCache.full_function_name == ctx.full_function_name,
278
+ FunctionCache.args_hash == ctx.args_hash,
279
+ )
280
+ .order_by(desc(FunctionCache.created_at))
281
+ )
282
+ if max_age is not None:
283
+ cutoff_time = utcnow() - max_age
284
+ statement = statement.where(FunctionCache.created_at >= cutoff_time)
285
+ cached_result = session.exec(statement).first()
286
+
287
+ if not cached_result:
288
+ return CacheLookup(hit=False)
289
+
290
+ if ctx.is_pydantic_model:
291
+ try:
292
+ value = convert_cached_output_to_pydantic(
293
+ ctx.return_type, cached_result.result
294
+ )
295
+ return CacheLookup(hit=True, value=value)
296
+ except (ValueError, TypeError) as e:
297
+ logger.warning(
298
+ f"{DB_CACHE_LOG_PREFIX} [cache-miss] Failed to validate cached result for {ctx.full_function_name}, treating as cache miss: {e}"
299
+ )
300
+ return CacheLookup(hit=False)
301
+
302
+ return CacheLookup(hit=True, value=cached_result.result)
303
+
304
+
305
+ def _save_cached(
306
+ api_keys: APIKeys,
307
+ ctx: CallContext,
308
+ computed_result: Any,
309
+ log_error_on_unsavable_data: bool,
310
+ ) -> None:
311
+ cache_entry = FunctionCache(
312
+ function_name=ctx.function_name,
313
+ full_function_name=ctx.full_function_name,
314
+ args_hash=ctx.args_hash,
315
+ args=ctx.args_dict,
316
+ result=computed_result,
317
+ created_at=utcnow(),
318
+ )
319
+ try:
320
+ with DBManager(
321
+ api_keys.sqlalchemy_db_url.get_secret_value()
322
+ ).get_session() as session:
323
+ logger.debug(
324
+ f"{DB_CACHE_LOG_PREFIX} [cache-save] Saving cache entry for {ctx.full_function_name}"
325
+ )
326
+ session.add(cache_entry)
327
+ session.commit()
328
+ except (DataError, psycopg2.errors.UntranslatableCharacter) as e:
329
+ (logger.error if log_error_on_unsavable_data else logger.warning)(
330
+ f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save cache entry for {ctx.full_function_name}: {e}"
331
+ )
332
+ except Exception:
333
+ logger.exception(
334
+ f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save cache entry for {ctx.full_function_name}"
335
+ )
234
336
 
235
337
 
236
338
  def contains_pydantic_model(return_type: Any) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.69.7.dev1114
3
+ Version: 0.69.9
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  License-File: LICENSE
6
6
  Author: Gnosis
@@ -19,6 +19,7 @@ prediction_market_agent_tooling/abis/ownable_erc721.abi.json,sha256=9sxm588MAQmq
19
19
  prediction_market_agent_tooling/abis/proxy.abi.json,sha256=h24GXZ6Q0bSZlwh7zOv0EiDvbqUz_PHtWfKHTyPJ1w4,644
20
20
  prediction_market_agent_tooling/abis/seer_gnosis_router.abi.json,sha256=DyADzOXhy9MDS31ReVrG7ibpWbw1jVy19nExZ80xfRY,6839
21
21
  prediction_market_agent_tooling/abis/seer_market_factory.abi.json,sha256=g7RVxZVUWlTXIgTV2W6kO4twQM909Qv58zAr7Dk4XIc,13553
22
+ prediction_market_agent_tooling/abis/swapr_quoter.abi.json,sha256=pm21GfVFY2yMYn3D0yKYZIYc_GQkX5GfE6lsGv6xl7c,5560
22
23
  prediction_market_agent_tooling/abis/swapr_router.abi.json,sha256=Y1wK20D1-zdbdlzkzV0BHY-HXMJrog6dgSHEzKzrQOU,13346
23
24
  prediction_market_agent_tooling/benchmark/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  prediction_market_agent_tooling/benchmark/agents.py,sha256=zC5tUM6pPTWtqSddOOSYV_fxHYmZb5uGAb4Ru87Tah8,4221
@@ -73,22 +74,22 @@ prediction_market_agent_tooling/markets/polymarket/polymarket.py,sha256=QKbmSflc
73
74
  prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py,sha256=x8yvAUPl55nbJALsQAAHJtstahhpH8WcEOj8or_QloQ,1165
74
75
  prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py,sha256=6_dJbp9YBz0TZW6PxWGIB1L9VE-1JAIzvf8qTBrEyYs,2905
75
76
  prediction_market_agent_tooling/markets/polymarket/utils.py,sha256=i1KjkQWABUoQyfX-XBuhACm6-POFMTXWifgP1V2sz-Y,1279
76
- prediction_market_agent_tooling/markets/seer/data_models.py,sha256=nnuclpajQCkb_s52cCf_rwGWdgjMutaIPyfUq7CqX4M,7626
77
+ prediction_market_agent_tooling/markets/seer/data_models.py,sha256=FjkrI761jXygGMDm9qdoIsCvNaPQc1sGaPcHqIGsjho,8148
77
78
  prediction_market_agent_tooling/markets/seer/exceptions.py,sha256=cEObdjluivD94tgOLzmimR7wgQEOt6SRakrYdhsRQtk,112
78
- prediction_market_agent_tooling/markets/seer/price_manager.py,sha256=Sz8NkbDd5CYruKe6KZ9GU--Jf8FDSkniE-PVrbqGiPM,9952
79
- prediction_market_agent_tooling/markets/seer/seer.py,sha256=1coDkaHX66s_lWHjzhLEgoaNGeOo87Sxd83e0YGgNDA,31153
79
+ prediction_market_agent_tooling/markets/seer/price_manager.py,sha256=xxApIc4L79vRcF-mmlOK39ZzuBui_H3JkVPgvqwbmj0,11085
80
+ prediction_market_agent_tooling/markets/seer/seer.py,sha256=L5QrpFqfP4jSQOAIZs96S5VEWHrG74Yx2t_Ce0rBWhs,31602
80
81
  prediction_market_agent_tooling/markets/seer/seer_api.py,sha256=4iiTWskxWm__oGgUUd1GhV_ItPSrAr0OfnYQ7lKndnQ,1143
81
- prediction_market_agent_tooling/markets/seer/seer_contracts.py,sha256=LEmZg6PO5GnxI2izxkUYtfU7W58MQJrp8CRoMnF-HSc,6170
82
+ prediction_market_agent_tooling/markets/seer/seer_contracts.py,sha256=0KHxAbUfLu-1cXBr5vBOCkavhKqskmGJBtaCqi5Y1r0,7153
82
83
  prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py,sha256=nmeSofxwtdB6osrP0ASmDWhL684U52Qp4bqJRbRFkCE,18794
83
84
  prediction_market_agent_tooling/markets/seer/subgraph_data_models.py,sha256=96v41jdNNxBqTCp5g8SLMSrIgeeITVx5IsBjVgm-qdo,2920
84
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py,sha256=k_sCEJZLroVDjOVkZ084VKJGNODLGjBGezhsWEZvlH4,3528
85
+ prediction_market_agent_tooling/markets/seer/swap_pool_handler.py,sha256=24RVIppztXXFkZp0df7GR_GUSslwR42XsPJhDZ-jphs,2612
85
86
  prediction_market_agent_tooling/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
87
  prediction_market_agent_tooling/tools/_generic_value.py,sha256=L9SH_-JwD6RQeKoeTxvNWbkY6CxWIz-VjZEURXoZleg,10617
87
88
  prediction_market_agent_tooling/tools/balances.py,sha256=Osab21btfJDw2Y-jT_TV-KHGrseCRxcsYeW6WcOMB8E,1050
88
89
  prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py,sha256=o5ba633gKiDqV4t_C2d9FWwH-HkRAOZd8FcZTYvbj6g,14451
89
90
  prediction_market_agent_tooling/tools/betting_strategies/stretch_bet_between.py,sha256=THMXwFlskvzbjnX_OiYtDSzI8XVFyULWfP2525_9UGc,429
90
91
  prediction_market_agent_tooling/tools/betting_strategies/utils.py,sha256=MpS3FOMn0C7nbmbQRUT9QwSh3UzzsgGczP91iSMr9wo,261
91
- prediction_market_agent_tooling/tools/caches/db_cache.py,sha256=rZIGhgijquwwPtp_qncSAPR1SDF2XxIVZL1ir0fgzWw,12127
92
+ prediction_market_agent_tooling/tools/caches/db_cache.py,sha256=V6o6UdesjkKzSJMhqkUtD76cJGPaNhuwA4OL2chIYSI,13801
92
93
  prediction_market_agent_tooling/tools/caches/inmemory_cache.py,sha256=ZW5iI5rmjqeAebu5T7ftRnlkxiL02IC-MxCfDB80x7w,1506
93
94
  prediction_market_agent_tooling/tools/caches/serializers.py,sha256=vFDx4fsPxclXp2q0sv27j4al_M_Tj9aR2JJP-xNHQXA,2151
94
95
  prediction_market_agent_tooling/tools/contract.py,sha256=BzpAFcbKl_KqwgAlaXx63Fg8jzr0EO3qEeOs1K11CPA,33905
@@ -136,8 +137,8 @@ prediction_market_agent_tooling/tools/tokens/usd.py,sha256=DPO-4HBTy1-TZHKL_9CnH
136
137
  prediction_market_agent_tooling/tools/transaction_cache.py,sha256=K5YKNL2_tR10Iw2TD9fuP-CTGpBbZtNdgbd0B_R7pjg,1814
137
138
  prediction_market_agent_tooling/tools/utils.py,sha256=ruq6P5TFs8CBHxeBLj1Plpx7kuNFPpDgMsJGQgDiRNs,8785
138
139
  prediction_market_agent_tooling/tools/web3_utils.py,sha256=CDbaidlLeQ4VHzSg150L7QNfHfGveljSePGuDVFEYqc,13963
139
- prediction_market_agent_tooling-0.69.7.dev1114.dist-info/METADATA,sha256=n1NsOqe19dtMJ1GoruC0i_INfeuaxR7SgxR5drTGy5Q,8898
140
- prediction_market_agent_tooling-0.69.7.dev1114.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
141
- prediction_market_agent_tooling-0.69.7.dev1114.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
142
- prediction_market_agent_tooling-0.69.7.dev1114.dist-info/licenses/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
143
- prediction_market_agent_tooling-0.69.7.dev1114.dist-info/RECORD,,
140
+ prediction_market_agent_tooling-0.69.9.dist-info/METADATA,sha256=fBezbmaBxLVzpMNtCsmCnE2Pc8ywbXJTnCRF7HMJcYw,8890
141
+ prediction_market_agent_tooling-0.69.9.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
142
+ prediction_market_agent_tooling-0.69.9.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
143
+ prediction_market_agent_tooling-0.69.9.dist-info/licenses/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
144
+ prediction_market_agent_tooling-0.69.9.dist-info/RECORD,,