prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__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.
- prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
- prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
- prediction_market_agent_tooling/abis/processor.abi.json +16 -0
- prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
- prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
- prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
- prediction_market_agent_tooling/benchmark/utils.py +13 -0
- prediction_market_agent_tooling/chains.py +1 -0
- prediction_market_agent_tooling/config.py +61 -2
- prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
- prediction_market_agent_tooling/deploy/agent.py +199 -67
- prediction_market_agent_tooling/deploy/agent_example.py +1 -1
- prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
- prediction_market_agent_tooling/deploy/constants.py +6 -0
- prediction_market_agent_tooling/gtypes.py +11 -1
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
- prediction_market_agent_tooling/loggers.py +9 -1
- prediction_market_agent_tooling/logprobs_parser.py +2 -1
- prediction_market_agent_tooling/markets/agent_market.py +106 -18
- prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
- prediction_market_agent_tooling/markets/data_models.py +120 -7
- prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
- prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
- prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
- prediction_market_agent_tooling/markets/market_type.py +74 -0
- prediction_market_agent_tooling/markets/markets.py +7 -99
- prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
- prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
- prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
- prediction_market_agent_tooling/markets/omen/omen.py +112 -23
- prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
- prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
- prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
- prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
- prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
- prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
- prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
- prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
- prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
- prediction_market_agent_tooling/markets/seer/seer.py +393 -106
- prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
- prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
- prediction_market_agent_tooling/tools/_generic_value.py +8 -2
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
- prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
- prediction_market_agent_tooling/tools/contract.py +480 -38
- prediction_market_agent_tooling/tools/contract_utils.py +61 -0
- prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
- prediction_market_agent_tooling/tools/cow/models.py +122 -0
- prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
- prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
- prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
- prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
- prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
- prediction_market_agent_tooling/tools/openai_utils.py +31 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
- prediction_market_agent_tooling/tools/rephrase.py +71 -0
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
- prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
- prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/utils.py +61 -3
- prediction_market_agent_tooling/tools/web3_utils.py +63 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
- prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
|
|
3
|
+
from prediction_market_agent_tooling.config import RPCConfig
|
|
4
|
+
from prediction_market_agent_tooling.gtypes import ChecksumAddress
|
|
5
|
+
from prediction_market_agent_tooling.tools.contract import contract_implements_function
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_erc20_contract(address: ChecksumAddress, web3: Web3 | None = None) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Checks if the given address is an ERC20-compatible contract.
|
|
11
|
+
|
|
12
|
+
It estimates it by looking if it implements symbol, name, decimals, totalSupply, balanceOf.
|
|
13
|
+
"""
|
|
14
|
+
web3 = web3 or RPCConfig().get_web3()
|
|
15
|
+
return (
|
|
16
|
+
contract_implements_function(address, "symbol", web3)
|
|
17
|
+
and contract_implements_function(address, "name", web3)
|
|
18
|
+
and contract_implements_function(address, "totalSupply", web3)
|
|
19
|
+
and contract_implements_function(
|
|
20
|
+
address, "balanceOf", web3, function_arg_types=["address"]
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_nft_contract(address: ChecksumAddress, web3: Web3 | None = None) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Checks if the given address is an NFT-compatible contract (ERC721 or ERC1155).
|
|
28
|
+
|
|
29
|
+
For ERC721, checks for: ownerOf, balanceOf, transferFrom.
|
|
30
|
+
For ERC1155, checks for: balanceOf, safeTransferFrom.
|
|
31
|
+
|
|
32
|
+
Returns True if either ERC721 or ERC1155 interface is detected.
|
|
33
|
+
"""
|
|
34
|
+
web3 = web3 or RPCConfig().get_web3()
|
|
35
|
+
is_erc721 = (
|
|
36
|
+
contract_implements_function(
|
|
37
|
+
address, "ownerOf", web3, function_arg_types=["uint256"]
|
|
38
|
+
)
|
|
39
|
+
and contract_implements_function(
|
|
40
|
+
address, "balanceOf", web3, function_arg_types=["address"]
|
|
41
|
+
)
|
|
42
|
+
and contract_implements_function(
|
|
43
|
+
address,
|
|
44
|
+
"transferFrom",
|
|
45
|
+
web3,
|
|
46
|
+
function_arg_types=["address", "address", "uint256"],
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
if is_erc721:
|
|
50
|
+
return True
|
|
51
|
+
is_erc1155 = contract_implements_function(
|
|
52
|
+
address, "balanceOf", web3, function_arg_types=["address", "uint256"]
|
|
53
|
+
) and contract_implements_function(
|
|
54
|
+
address,
|
|
55
|
+
"safeTransferFrom",
|
|
56
|
+
web3,
|
|
57
|
+
function_arg_types=["address", "address", "uint256", "uint256", "bytes"],
|
|
58
|
+
)
|
|
59
|
+
if is_erc1155:
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import typing as t
|
|
2
3
|
from datetime import timedelta
|
|
3
4
|
|
|
4
5
|
import httpx
|
|
@@ -8,11 +9,17 @@ from cowdao_cowpy.common.api.errors import UnexpectedResponseError
|
|
|
8
9
|
from cowdao_cowpy.common.chains import Chain
|
|
9
10
|
from cowdao_cowpy.common.config import SupportedChainId
|
|
10
11
|
from cowdao_cowpy.common.constants import CowContractAddress
|
|
12
|
+
from cowdao_cowpy.contracts.domain import domain
|
|
13
|
+
from cowdao_cowpy.contracts.sign import SigningScheme, sign_order_cancellations
|
|
11
14
|
from cowdao_cowpy.cow.swap import CompletedOrder, get_order_quote, swap_tokens
|
|
12
15
|
from cowdao_cowpy.order_book.api import OrderBookApi
|
|
13
16
|
from cowdao_cowpy.order_book.config import Envs, OrderBookAPIConfigFactory
|
|
14
17
|
from cowdao_cowpy.order_book.generated.model import (
|
|
18
|
+
UID,
|
|
15
19
|
Address,
|
|
20
|
+
EcdsaSignature,
|
|
21
|
+
EcdsaSigningScheme,
|
|
22
|
+
OrderCancellations,
|
|
16
23
|
OrderMetaData,
|
|
17
24
|
OrderQuoteRequest,
|
|
18
25
|
OrderQuoteResponse,
|
|
@@ -23,28 +30,28 @@ from cowdao_cowpy.order_book.generated.model import (
|
|
|
23
30
|
OrderStatus,
|
|
24
31
|
TokenAmount,
|
|
25
32
|
)
|
|
26
|
-
from
|
|
27
|
-
from
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
wait_exponential,
|
|
31
|
-
wait_fixed,
|
|
32
|
-
)
|
|
33
|
+
from eth_account import Account
|
|
34
|
+
from eth_account.signers.local import LocalAccount
|
|
35
|
+
from eth_keys.datatypes import PrivateKey as eth_keys_PrivateKey
|
|
36
|
+
from tenacity import stop_after_attempt, wait_exponential, wait_fixed
|
|
33
37
|
from web3 import Web3
|
|
34
38
|
|
|
35
39
|
from prediction_market_agent_tooling.config import APIKeys
|
|
36
|
-
from prediction_market_agent_tooling.gtypes import
|
|
40
|
+
from prediction_market_agent_tooling.gtypes import (
|
|
41
|
+
ChecksumAddress,
|
|
42
|
+
HexBytes,
|
|
43
|
+
HexStr,
|
|
44
|
+
PrivateKey,
|
|
45
|
+
Wei,
|
|
46
|
+
)
|
|
37
47
|
from prediction_market_agent_tooling.loggers import logger
|
|
38
48
|
from prediction_market_agent_tooling.markets.omen.cow_contracts import (
|
|
39
49
|
CowGPv2SettlementContract,
|
|
40
50
|
)
|
|
41
|
-
from prediction_market_agent_tooling.tools.contract import
|
|
42
|
-
from prediction_market_agent_tooling.tools.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class MinimalisticToken(BaseModel):
|
|
46
|
-
sellToken: ChecksumAddress
|
|
47
|
-
buyToken: ChecksumAddress
|
|
51
|
+
from prediction_market_agent_tooling.tools.contract import ContractERC20BaseClass
|
|
52
|
+
from prediction_market_agent_tooling.tools.cow.models import MinimalisticTrade, Order
|
|
53
|
+
from prediction_market_agent_tooling.tools.cow.semaphore import postgres_rate_limited
|
|
54
|
+
from prediction_market_agent_tooling.tools.utils import utcnow
|
|
48
55
|
|
|
49
56
|
|
|
50
57
|
class OrderStatusError(Exception):
|
|
@@ -96,7 +103,6 @@ def get_sell_token_amount(
|
|
|
96
103
|
@tenacity.retry(
|
|
97
104
|
stop=stop_after_attempt(4),
|
|
98
105
|
wait=wait_exponential(min=4, max=10),
|
|
99
|
-
retry=retry_if_not_exception_type(NoLiquidityAvailableOnCowException),
|
|
100
106
|
)
|
|
101
107
|
def get_quote(
|
|
102
108
|
amount_wei: Wei,
|
|
@@ -157,10 +163,41 @@ def get_buy_token_amount_else_raise(
|
|
|
157
163
|
return Wei(order_quote.quote.buyAmount.root)
|
|
158
164
|
|
|
159
165
|
|
|
166
|
+
def handle_allowance(
|
|
167
|
+
api_keys: APIKeys,
|
|
168
|
+
sell_token: ChecksumAddress,
|
|
169
|
+
amount_to_check_wei: Wei,
|
|
170
|
+
amount_to_set_wei: Wei | None = None,
|
|
171
|
+
for_address: ChecksumAddress | None = None,
|
|
172
|
+
web3: Web3 | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
# Approve the CoW Swap Vault Relayer to get the sell token only if allowance not sufficient.
|
|
175
|
+
for_address = for_address or Web3.to_checksum_address(
|
|
176
|
+
CowContractAddress.VAULT_RELAYER.value
|
|
177
|
+
)
|
|
178
|
+
current_allowance = ContractERC20BaseClass(address=sell_token).allowance(
|
|
179
|
+
owner=api_keys.bet_from_address,
|
|
180
|
+
for_address=for_address,
|
|
181
|
+
web3=web3,
|
|
182
|
+
)
|
|
183
|
+
if current_allowance < amount_to_check_wei:
|
|
184
|
+
amount_to_set_wei = amount_to_set_wei or amount_to_check_wei
|
|
185
|
+
ContractERC20BaseClass(address=sell_token).approve(
|
|
186
|
+
api_keys,
|
|
187
|
+
for_address=for_address,
|
|
188
|
+
amount_wei=amount_to_set_wei,
|
|
189
|
+
web3=web3,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@postgres_rate_limited(
|
|
194
|
+
api_keys=APIKeys(), rate_id="swap_tokens_waiting", interval_seconds=5.0
|
|
195
|
+
)
|
|
160
196
|
@tenacity.retry(
|
|
197
|
+
reraise=True,
|
|
161
198
|
stop=stop_after_attempt(3),
|
|
162
199
|
wait=wait_fixed(1),
|
|
163
|
-
retry=tenacity.retry_if_not_exception_type((TimeoutError
|
|
200
|
+
retry=tenacity.retry_if_not_exception_type((TimeoutError)),
|
|
164
201
|
after=lambda x: logger.debug(f"swap_tokens_waiting failed, {x.attempt_number=}."),
|
|
165
202
|
)
|
|
166
203
|
def swap_tokens_waiting(
|
|
@@ -171,40 +208,37 @@ def swap_tokens_waiting(
|
|
|
171
208
|
chain: Chain = Chain.GNOSIS,
|
|
172
209
|
env: Envs = "prod",
|
|
173
210
|
web3: Web3 | None = None,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
owner=api_keys.bet_from_address,
|
|
179
|
-
for_address=for_address,
|
|
180
|
-
web3=web3,
|
|
181
|
-
)
|
|
182
|
-
if current_allowance < amount_wei:
|
|
183
|
-
ContractERC20OnGnosisChain(address=sell_token).approve(
|
|
184
|
-
api_keys,
|
|
185
|
-
for_address=for_address,
|
|
186
|
-
amount_wei=amount_wei,
|
|
187
|
-
web3=web3,
|
|
188
|
-
)
|
|
189
|
-
|
|
211
|
+
wait_order_complete: bool = True,
|
|
212
|
+
timeout: timedelta = timedelta(seconds=120),
|
|
213
|
+
slippage_tolerance: float = 0.01,
|
|
214
|
+
) -> tuple[OrderMetaData | None, CompletedOrder]:
|
|
190
215
|
# CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
|
|
191
216
|
return asyncio.run(
|
|
192
217
|
swap_tokens_waiting_async(
|
|
193
|
-
amount_wei,
|
|
218
|
+
amount_wei,
|
|
219
|
+
sell_token,
|
|
220
|
+
buy_token,
|
|
221
|
+
api_keys,
|
|
222
|
+
chain,
|
|
223
|
+
env,
|
|
224
|
+
timeout=timeout,
|
|
225
|
+
web3=web3,
|
|
226
|
+
wait_order_complete=wait_order_complete,
|
|
227
|
+
slippage_tolerance=slippage_tolerance,
|
|
194
228
|
)
|
|
195
229
|
)
|
|
196
230
|
|
|
197
231
|
|
|
198
|
-
async def
|
|
232
|
+
async def place_swap_order(
|
|
233
|
+
api_keys: APIKeys,
|
|
199
234
|
amount_wei: Wei,
|
|
200
235
|
sell_token: ChecksumAddress,
|
|
201
236
|
buy_token: ChecksumAddress,
|
|
202
|
-
api_keys: APIKeys,
|
|
203
237
|
chain: Chain,
|
|
204
238
|
env: Envs,
|
|
205
|
-
timeout: timedelta = timedelta(seconds=120),
|
|
206
239
|
slippage_tolerance: float = 0.01,
|
|
207
|
-
|
|
240
|
+
valid_to: int | None = None,
|
|
241
|
+
) -> CompletedOrder:
|
|
208
242
|
account = api_keys.get_account()
|
|
209
243
|
safe_address = api_keys.safe_address_checksum
|
|
210
244
|
|
|
@@ -217,6 +251,7 @@ async def swap_tokens_waiting_async(
|
|
|
217
251
|
chain=chain,
|
|
218
252
|
env=env,
|
|
219
253
|
slippage_tolerance=slippage_tolerance,
|
|
254
|
+
valid_to=valid_to,
|
|
220
255
|
)
|
|
221
256
|
logger.info(f"Order created: {order}")
|
|
222
257
|
|
|
@@ -224,6 +259,12 @@ async def swap_tokens_waiting_async(
|
|
|
224
259
|
logger.info(f"Safe is used. Signing the order after its creation.")
|
|
225
260
|
await sign_safe_cow_swap(api_keys, order, chain, env)
|
|
226
261
|
|
|
262
|
+
return order
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def wait_for_order_completion(
|
|
266
|
+
order: CompletedOrder, timeout: timedelta = timedelta(seconds=120)
|
|
267
|
+
) -> OrderMetaData:
|
|
227
268
|
start_time = utcnow()
|
|
228
269
|
|
|
229
270
|
while True:
|
|
@@ -253,6 +294,41 @@ async def swap_tokens_waiting_async(
|
|
|
253
294
|
await asyncio.sleep(3.14)
|
|
254
295
|
|
|
255
296
|
|
|
297
|
+
async def swap_tokens_waiting_async(
|
|
298
|
+
amount_wei: Wei,
|
|
299
|
+
sell_token: ChecksumAddress,
|
|
300
|
+
buy_token: ChecksumAddress,
|
|
301
|
+
api_keys: APIKeys,
|
|
302
|
+
chain: Chain,
|
|
303
|
+
env: Envs,
|
|
304
|
+
timeout: timedelta = timedelta(seconds=120),
|
|
305
|
+
slippage_tolerance: float = 0.01,
|
|
306
|
+
web3: Web3 | None = None,
|
|
307
|
+
wait_order_complete: bool = True,
|
|
308
|
+
) -> tuple[OrderMetaData | None, CompletedOrder]:
|
|
309
|
+
handle_allowance(
|
|
310
|
+
api_keys=api_keys,
|
|
311
|
+
sell_token=sell_token,
|
|
312
|
+
amount_to_check_wei=amount_wei,
|
|
313
|
+
web3=web3,
|
|
314
|
+
)
|
|
315
|
+
valid_to = (utcnow() + timeout).timestamp()
|
|
316
|
+
order = await place_swap_order(
|
|
317
|
+
api_keys=api_keys,
|
|
318
|
+
amount_wei=amount_wei,
|
|
319
|
+
sell_token=sell_token,
|
|
320
|
+
buy_token=buy_token,
|
|
321
|
+
chain=chain,
|
|
322
|
+
env=env,
|
|
323
|
+
slippage_tolerance=slippage_tolerance,
|
|
324
|
+
valid_to=int(valid_to),
|
|
325
|
+
)
|
|
326
|
+
if wait_order_complete:
|
|
327
|
+
order_metadata = await wait_for_order_completion(order=order, timeout=timeout)
|
|
328
|
+
return order_metadata, order
|
|
329
|
+
return None, order
|
|
330
|
+
|
|
331
|
+
|
|
256
332
|
@tenacity.retry(
|
|
257
333
|
stop=tenacity.stop_after_attempt(3),
|
|
258
334
|
wait=tenacity.wait_fixed(1),
|
|
@@ -266,11 +342,8 @@ async def sign_safe_cow_swap(
|
|
|
266
342
|
) -> None:
|
|
267
343
|
order_book_api = get_order_book_api(env, chain)
|
|
268
344
|
posted_order = await order_book_api.get_order_by_uid(order.uid)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
check_not_none(posted_order.settlementContract).root
|
|
272
|
-
)
|
|
273
|
-
).setPreSignature(
|
|
345
|
+
|
|
346
|
+
CowGPv2SettlementContract().setPreSignature(
|
|
274
347
|
api_keys,
|
|
275
348
|
HexBytes(posted_order.uid.root),
|
|
276
349
|
True,
|
|
@@ -284,11 +357,111 @@ async def sign_safe_cow_swap(
|
|
|
284
357
|
)
|
|
285
358
|
def get_trades_by_owner(
|
|
286
359
|
owner: ChecksumAddress,
|
|
287
|
-
) -> list[
|
|
360
|
+
) -> list[MinimalisticTrade]:
|
|
288
361
|
# Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
|
|
289
362
|
response = httpx.get(
|
|
290
363
|
f"https://api.cow.fi/xdai/api/v1/trades",
|
|
291
364
|
params={"owner": owner},
|
|
292
365
|
)
|
|
293
366
|
response.raise_for_status()
|
|
294
|
-
return [
|
|
367
|
+
return [MinimalisticTrade.model_validate(i) for i in response.json()]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@tenacity.retry(
|
|
371
|
+
stop=stop_after_attempt(3),
|
|
372
|
+
wait=wait_fixed(1),
|
|
373
|
+
after=lambda x: logger.debug(
|
|
374
|
+
f"get_trades_by_order_uid failed, {x.attempt_number=}."
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
def get_trades_by_order_uid(
|
|
378
|
+
order_uid: HexBytes,
|
|
379
|
+
) -> list[MinimalisticTrade]:
|
|
380
|
+
# Using this until cowpy gets fixed (https://github.com/cowdao-grants/cow-py/issues/35)
|
|
381
|
+
response = httpx.get(
|
|
382
|
+
f"https://api.cow.fi/xdai/api/v1/trades",
|
|
383
|
+
params={"orderUid": order_uid.to_0x_hex()},
|
|
384
|
+
)
|
|
385
|
+
response.raise_for_status()
|
|
386
|
+
return [MinimalisticTrade.model_validate(i) for i in response.json()]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@tenacity.retry(
|
|
390
|
+
stop=stop_after_attempt(3),
|
|
391
|
+
wait=wait_fixed(1),
|
|
392
|
+
after=lambda x: logger.debug(f"get_orders_by_owner failed, {x.attempt_number=}."),
|
|
393
|
+
)
|
|
394
|
+
def get_orders_by_owner(
|
|
395
|
+
owner: ChecksumAddress,
|
|
396
|
+
) -> list[Order]:
|
|
397
|
+
"""Retrieves all orders with pagination."""
|
|
398
|
+
items = paginate_endpoint(f"https://api.cow.fi/xdai/api/v1/account/{owner}/orders")
|
|
399
|
+
return [Order.model_validate(i) for i in items]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@tenacity.retry(
|
|
403
|
+
stop=stop_after_attempt(3),
|
|
404
|
+
wait=wait_fixed(1),
|
|
405
|
+
after=lambda x: logger.debug(f"get_order_by_uid failed, {x.attempt_number=}."),
|
|
406
|
+
)
|
|
407
|
+
async def get_order_by_uid(
|
|
408
|
+
uid: HexBytes,
|
|
409
|
+
) -> Order:
|
|
410
|
+
async with httpx.AsyncClient() as client:
|
|
411
|
+
response = await client.get(
|
|
412
|
+
f"https://api.cow.fi/xdai/api/v1/orders/{uid.to_0x_hex()}"
|
|
413
|
+
)
|
|
414
|
+
response.raise_for_status()
|
|
415
|
+
return Order.model_validate(response.json())
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@tenacity.retry(
|
|
419
|
+
stop=stop_after_attempt(3),
|
|
420
|
+
wait=wait_fixed(1),
|
|
421
|
+
after=lambda x: logger.debug(f"paginate_endpoint failed, {x.attempt_number=}."),
|
|
422
|
+
)
|
|
423
|
+
def paginate_endpoint(url: str, limit: int = 1000) -> t.Any:
|
|
424
|
+
offset = 0
|
|
425
|
+
results = []
|
|
426
|
+
|
|
427
|
+
while True:
|
|
428
|
+
response = httpx.get(url, params={"offset": offset, "limit": limit})
|
|
429
|
+
response.raise_for_status()
|
|
430
|
+
items = response.json()
|
|
431
|
+
if not items:
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
results.extend(items)
|
|
435
|
+
offset += limit
|
|
436
|
+
|
|
437
|
+
return results
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
async def cancel_order(
|
|
441
|
+
order_uids: t.Sequence[str],
|
|
442
|
+
api_keys: APIKeys,
|
|
443
|
+
chain: Chain = Chain.GNOSIS,
|
|
444
|
+
env: Envs = "prod",
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Cancels orders that were created by the user corresponding to the api_keys.."""
|
|
447
|
+
order_domain = domain(
|
|
448
|
+
chain=chain, verifying_contract=CowContractAddress.SETTLEMENT_CONTRACT.value
|
|
449
|
+
)
|
|
450
|
+
owner = build_account(api_keys.bet_from_private_key)
|
|
451
|
+
order_signature = sign_order_cancellations(
|
|
452
|
+
order_domain, list(order_uids), owner, SigningScheme.EIP712
|
|
453
|
+
)
|
|
454
|
+
oc = OrderCancellations(
|
|
455
|
+
signature=EcdsaSignature(root=order_signature.data),
|
|
456
|
+
orderUids=[UID(root=order_uid) for order_uid in order_uids],
|
|
457
|
+
signingScheme=EcdsaSigningScheme.eip712,
|
|
458
|
+
)
|
|
459
|
+
order_book_api = get_order_book_api(env, chain)
|
|
460
|
+
await order_book_api.delete_order(orders_cancelation=oc)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def build_account(private_key: PrivateKey) -> LocalAccount:
|
|
464
|
+
return LocalAccount(
|
|
465
|
+
key=eth_keys_PrivateKey(HexBytes(HexStr(private_key.get_secret_value()))),
|
|
466
|
+
account=Account.from_key(private_key.get_secret_value()),
|
|
467
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional, TypeAlias
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from sqlmodel import Field, SQLModel
|
|
6
|
+
|
|
7
|
+
from prediction_market_agent_tooling.gtypes import (
|
|
8
|
+
HexBytes,
|
|
9
|
+
VerifiedChecksumAddress,
|
|
10
|
+
VerifiedChecksumAddressOrNone,
|
|
11
|
+
Wei,
|
|
12
|
+
)
|
|
13
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
14
|
+
from prediction_market_agent_tooling.tools.utils import utcnow
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MinimalisticTrade(BaseModel):
|
|
18
|
+
sellToken: VerifiedChecksumAddress
|
|
19
|
+
buyToken: VerifiedChecksumAddress
|
|
20
|
+
orderUid: HexBytes
|
|
21
|
+
txHash: HexBytes
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EthFlowData(BaseModel):
|
|
25
|
+
refundTxHash: Optional[HexBytes]
|
|
26
|
+
userValidTo: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PlacementError(str, Enum):
|
|
30
|
+
QuoteNotFound = "QuoteNotFound"
|
|
31
|
+
ValidToTooFarInFuture = "ValidToTooFarInFuture"
|
|
32
|
+
PreValidationError = "PreValidationError"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OnchainOrderData(BaseModel):
|
|
36
|
+
sender: VerifiedChecksumAddress
|
|
37
|
+
placementError: Optional[PlacementError]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OrderKind(str, Enum):
|
|
41
|
+
buy = "buy"
|
|
42
|
+
sell = "sell"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SellTokenBalance(str, Enum):
|
|
46
|
+
external = "external"
|
|
47
|
+
internal = "internal"
|
|
48
|
+
erc20 = "erc20"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BuyTokenBalance(str, Enum):
|
|
52
|
+
internal = "internal"
|
|
53
|
+
erc20 = "erc20"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SigningScheme(str, Enum):
|
|
57
|
+
eip712 = "eip712"
|
|
58
|
+
ethsign = "ethsign"
|
|
59
|
+
presign = "presign"
|
|
60
|
+
eip1271 = "eip1271"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OrderClass(str, Enum):
|
|
64
|
+
limit = "limit"
|
|
65
|
+
liquidity = "liquidity"
|
|
66
|
+
market = "market"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OrderStatus(str, Enum):
|
|
70
|
+
presignaturePending = "presignaturePending"
|
|
71
|
+
open = "open"
|
|
72
|
+
fulfilled = "fulfilled"
|
|
73
|
+
cancelled = "cancelled"
|
|
74
|
+
expired = "expired"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
CowOrderUID: TypeAlias = HexBytes
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Order(BaseModel):
|
|
81
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
82
|
+
|
|
83
|
+
uid: CowOrderUID
|
|
84
|
+
quoteId: int | None = None
|
|
85
|
+
validTo: int
|
|
86
|
+
sellAmount: Wei
|
|
87
|
+
sellToken: VerifiedChecksumAddress
|
|
88
|
+
buyAmount: Wei
|
|
89
|
+
buyToken: VerifiedChecksumAddress
|
|
90
|
+
receiver: VerifiedChecksumAddressOrNone
|
|
91
|
+
feeAmount: Wei
|
|
92
|
+
creationDate: DatetimeUTC
|
|
93
|
+
kind: OrderKind
|
|
94
|
+
partiallyFillable: bool
|
|
95
|
+
sellTokenBalance: SellTokenBalance | None
|
|
96
|
+
buyTokenBalance: BuyTokenBalance | None
|
|
97
|
+
signingScheme: SigningScheme
|
|
98
|
+
signature: HexBytes
|
|
99
|
+
from_: VerifiedChecksumAddressOrNone = Field(None, alias="from")
|
|
100
|
+
appData: str
|
|
101
|
+
fullAppData: str | None
|
|
102
|
+
appDataHash: HexBytes | None = None
|
|
103
|
+
class_: str = Field(None, alias="class")
|
|
104
|
+
owner: VerifiedChecksumAddress
|
|
105
|
+
executedSellAmount: Wei
|
|
106
|
+
executedSellAmountBeforeFees: Wei
|
|
107
|
+
executedBuyAmount: Wei
|
|
108
|
+
executedFeeAmount: Wei | None
|
|
109
|
+
invalidated: bool
|
|
110
|
+
status: str
|
|
111
|
+
isLiquidityOrder: bool | None
|
|
112
|
+
ethflowData: EthFlowData | None = None
|
|
113
|
+
onchainUser: VerifiedChecksumAddressOrNone = None
|
|
114
|
+
executedFee: Wei
|
|
115
|
+
executedFeeToken: VerifiedChecksumAddressOrNone
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RateLimit(SQLModel, table=True):
|
|
119
|
+
__tablename__ = "rate_limit"
|
|
120
|
+
__table_args__ = {"extend_existing": True}
|
|
121
|
+
id: str = Field(primary_key=True)
|
|
122
|
+
last_called_at: DatetimeUTC = Field(default_factory=utcnow)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any, Callable, Optional, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.exc import OperationalError
|
|
7
|
+
from sqlmodel import Session, select
|
|
8
|
+
|
|
9
|
+
from prediction_market_agent_tooling.config import APIKeys
|
|
10
|
+
from prediction_market_agent_tooling.tools.cow.models import RateLimit
|
|
11
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
12
|
+
from prediction_market_agent_tooling.tools.db.db_manager import DBManager
|
|
13
|
+
from prediction_market_agent_tooling.tools.utils import utcnow
|
|
14
|
+
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
|
+
|
|
17
|
+
FALLBACK_SQL_ENGINE = "sqlite:///rate_limit.db"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def postgres_rate_limited(
|
|
21
|
+
api_keys: APIKeys,
|
|
22
|
+
rate_id: str,
|
|
23
|
+
interval_seconds: float,
|
|
24
|
+
shared_db: bool = False,
|
|
25
|
+
) -> Callable[[F], F]:
|
|
26
|
+
"""rate_id is used to distinguish between different rate limits for different functions"""
|
|
27
|
+
limiter = RateLimiter(id=rate_id, interval_seconds=interval_seconds)
|
|
28
|
+
|
|
29
|
+
def decorator(func: F) -> F:
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
sqlalchemy_db_url = (
|
|
33
|
+
api_keys.sqlalchemy_db_url.get_secret_value()
|
|
34
|
+
if shared_db
|
|
35
|
+
else FALLBACK_SQL_ENGINE
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
db_manager = DBManager(sqlalchemy_db_url)
|
|
39
|
+
db_manager.create_tables([RateLimit])
|
|
40
|
+
|
|
41
|
+
with db_manager.get_session() as session:
|
|
42
|
+
limiter.enforce(session)
|
|
43
|
+
return func(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
return cast(F, wrapper)
|
|
46
|
+
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RateLimiter:
|
|
51
|
+
def __init__(self, id: str, interval_seconds: float = 1.0) -> None:
|
|
52
|
+
self.id = id
|
|
53
|
+
self.interval = timedelta(seconds=interval_seconds)
|
|
54
|
+
|
|
55
|
+
def enforce(self, session: Session, timeout_seconds: float = 30.0) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Enforces the rate limit inside a transaction.
|
|
58
|
+
Blocks until allowed or timeout is reached.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
session: The database session to use
|
|
62
|
+
timeout_seconds: Maximum time in seconds to wait before giving up
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
TimeoutError: If the rate limit cannot be acquired within the timeout period
|
|
66
|
+
"""
|
|
67
|
+
start_time = time.monotonic()
|
|
68
|
+
|
|
69
|
+
while True:
|
|
70
|
+
try:
|
|
71
|
+
with session.begin():
|
|
72
|
+
stmt = (
|
|
73
|
+
select(RateLimit)
|
|
74
|
+
.where(RateLimit.id == self.id)
|
|
75
|
+
.with_for_update()
|
|
76
|
+
)
|
|
77
|
+
result: Optional[RateLimit] = session.exec(stmt).first()
|
|
78
|
+
|
|
79
|
+
now = utcnow()
|
|
80
|
+
|
|
81
|
+
if result is None:
|
|
82
|
+
# First time this limiter is used
|
|
83
|
+
session.add(RateLimit(id=self.id))
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
last_called_aware = DatetimeUTC.from_datetime(result.last_called_at)
|
|
87
|
+
elapsed = now - last_called_aware
|
|
88
|
+
if elapsed >= self.interval:
|
|
89
|
+
result.last_called_at = now
|
|
90
|
+
session.add(result)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Not enough time passed, sleep and retry
|
|
94
|
+
to_sleep = (self.interval - elapsed).total_seconds()
|
|
95
|
+
time.sleep(to_sleep)
|
|
96
|
+
except OperationalError:
|
|
97
|
+
# Backoff if DB is under contention
|
|
98
|
+
elapsed_time = time.monotonic() - start_time
|
|
99
|
+
if elapsed_time > timeout_seconds:
|
|
100
|
+
raise TimeoutError(
|
|
101
|
+
f"Could not acquire rate limit '{self.id}' "
|
|
102
|
+
f"after {elapsed_time:.1f} seconds due to database contention"
|
|
103
|
+
)
|
|
104
|
+
time.sleep(0.5)
|
|
@@ -34,8 +34,20 @@ class DatetimeUTC(datetime):
|
|
|
34
34
|
def __get_pydantic_core_schema__(
|
|
35
35
|
cls, source_type: t.Any, handler: GetCoreSchemaHandler
|
|
36
36
|
) -> CoreSchema:
|
|
37
|
-
|
|
38
|
-
return core_schema.
|
|
37
|
+
# Use union schema to handle int, str, and datetime inputs directly
|
|
38
|
+
return core_schema.union_schema(
|
|
39
|
+
[
|
|
40
|
+
core_schema.no_info_after_validator_function(
|
|
41
|
+
cls._validate, core_schema.int_schema()
|
|
42
|
+
),
|
|
43
|
+
core_schema.no_info_after_validator_function(
|
|
44
|
+
cls._validate, core_schema.str_schema()
|
|
45
|
+
),
|
|
46
|
+
core_schema.no_info_after_validator_function(
|
|
47
|
+
cls._validate, handler(datetime)
|
|
48
|
+
),
|
|
49
|
+
]
|
|
50
|
+
)
|
|
39
51
|
|
|
40
52
|
@staticmethod
|
|
41
53
|
def from_datetime(dt: datetime) -> "DatetimeUTC":
|