poly-web3 0.0.5__tar.gz → 1.0.0__tar.gz
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.
- {poly_web3-0.0.5 → poly_web3-1.0.0}/PKG-INFO +1 -1
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/log.py +6 -15
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/web3_service/base.py +129 -5
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/web3_service/eoa_service.py +5 -3
- poly_web3-1.0.0/poly_web3/web3_service/proxy_service.py +125 -0
- poly_web3-1.0.0/poly_web3/web3_service/safe_service.py +24 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3.egg-info/PKG-INFO +1 -1
- {poly_web3-0.0.5 → poly_web3-1.0.0}/pyproject.toml +1 -1
- poly_web3-0.0.5/poly_web3/web3_service/proxy_service.py +0 -235
- poly_web3-0.0.5/poly_web3/web3_service/safe_service.py +0 -17
- {poly_web3-0.0.5 → poly_web3-1.0.0}/README.md +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/examples/example_redeem.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/__init__.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/const.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/schema.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/signature/__init__.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/signature/build.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/signature/hash_message.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/signature/secp256k1.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3/web3_service/__init__.py +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3.egg-info/SOURCES.txt +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3.egg-info/dependency_links.txt +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3.egg-info/requires.txt +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/poly_web3.egg-info/top_level.txt +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/setup.cfg +0 -0
- {poly_web3-0.0.5 → poly_web3-1.0.0}/setup.py +0 -0
|
@@ -9,15 +9,17 @@ import sys
|
|
|
9
9
|
|
|
10
10
|
LOGGER_NAME = "poly_web3"
|
|
11
11
|
|
|
12
|
-
logger = logging.getLogger(LOGGER_NAME)
|
|
13
|
-
logger.addHandler(logging.NullHandler())
|
|
14
|
-
|
|
15
12
|
|
|
16
13
|
def configure_logging(
|
|
17
14
|
level: int = logging.INFO,
|
|
18
15
|
stream=sys.stdout,
|
|
19
16
|
formatter: logging.Formatter | None = None,
|
|
20
17
|
):
|
|
18
|
+
try:
|
|
19
|
+
from loguru import logger
|
|
20
|
+
return logger
|
|
21
|
+
except:
|
|
22
|
+
pass
|
|
21
23
|
if formatter is None:
|
|
22
24
|
formatter = logging.Formatter(
|
|
23
25
|
"%(asctime)s | %(levelname)s | %(name)s | %(filename)s:%(lineno)d %(message)s",
|
|
@@ -34,15 +36,4 @@ def configure_logging(
|
|
|
34
36
|
return target
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
target = logging.getLogger(LOGGER_NAME)
|
|
39
|
-
if target.handlers and not all(
|
|
40
|
-
isinstance(h, logging.NullHandler) for h in target.handlers
|
|
41
|
-
):
|
|
42
|
-
return
|
|
43
|
-
if logging.getLogger().handlers:
|
|
44
|
-
return
|
|
45
|
-
configure_logging()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
_ensure_default_logging()
|
|
39
|
+
logger = configure_logging()
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
# @Site:
|
|
5
5
|
# @File: base.py
|
|
6
6
|
# @Software: PyCharm
|
|
7
|
-
import
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
8
9
|
from py_builder_relayer_client.client import RelayClient
|
|
9
10
|
from py_clob_client.client import ClobClient
|
|
10
11
|
from web3 import Web3
|
|
12
|
+
import requests
|
|
11
13
|
|
|
12
14
|
from poly_web3.const import (
|
|
13
15
|
RPC_URL,
|
|
@@ -166,6 +168,107 @@ class BaseWeb3Service:
|
|
|
166
168
|
redeem_amounts,
|
|
167
169
|
)._encode_transaction_data()
|
|
168
170
|
|
|
171
|
+
def _build_redeem_tx(self, to: str, data: str) -> Any:
|
|
172
|
+
raise NotImplementedError("redeem tx builder not implemented")
|
|
173
|
+
|
|
174
|
+
def _build_redeem_txs_from_positions(self, positions: list[dict]) -> list[Any]:
|
|
175
|
+
neg_amounts_by_condition: dict[str, list[float]] = {}
|
|
176
|
+
normal_conditions: set[str] = set()
|
|
177
|
+
for pos in positions:
|
|
178
|
+
condition_id = pos.get("conditionId")
|
|
179
|
+
if not condition_id:
|
|
180
|
+
continue
|
|
181
|
+
if pos.get("negativeRisk"):
|
|
182
|
+
idx = pos.get("outcomeIndex")
|
|
183
|
+
size = pos.get("size")
|
|
184
|
+
if idx is None or size is None:
|
|
185
|
+
continue
|
|
186
|
+
amounts = neg_amounts_by_condition.setdefault(condition_id, [0.0, 0.0])
|
|
187
|
+
if idx not in (0, 1):
|
|
188
|
+
raise Exception(f"negRisk outcomeIndex out of range: {idx}")
|
|
189
|
+
amounts[idx] += size
|
|
190
|
+
else:
|
|
191
|
+
normal_conditions.add(condition_id)
|
|
192
|
+
txs: list[Any] = []
|
|
193
|
+
for condition_id, amounts in neg_amounts_by_condition.items():
|
|
194
|
+
int_amounts = [int(amount * 1e6) for amount in amounts]
|
|
195
|
+
txs.append(
|
|
196
|
+
self._build_redeem_tx(
|
|
197
|
+
NEG_RISK_ADAPTER_ADDRESS,
|
|
198
|
+
self.build_neg_risk_redeem_tx_data(condition_id, int_amounts),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
for condition_id in normal_conditions:
|
|
202
|
+
txs.append(
|
|
203
|
+
self._build_redeem_tx(
|
|
204
|
+
CTF_ADDRESS,
|
|
205
|
+
self.build_ctf_redeem_tx_data(condition_id),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return txs
|
|
209
|
+
|
|
210
|
+
def _submit_redeem(self, txs: list[Any]) -> dict | None:
|
|
211
|
+
raise NotImplementedError("redeem submit not implemented")
|
|
212
|
+
|
|
213
|
+
def _redeem_batch(self, condition_ids: list[str], batch_size: int) -> list[dict]:
|
|
214
|
+
"""
|
|
215
|
+
Fetch positions by condition IDs in batches, then redeem each batch.
|
|
216
|
+
"""
|
|
217
|
+
if not condition_ids:
|
|
218
|
+
return []
|
|
219
|
+
user_address = self._resolve_user_address()
|
|
220
|
+
redeem_list = []
|
|
221
|
+
for batch in self._chunk_condition_ids(condition_ids, batch_size):
|
|
222
|
+
positions = self.fetch_positions_by_condition_ids(
|
|
223
|
+
user_address=user_address, condition_ids=batch
|
|
224
|
+
)
|
|
225
|
+
redeem_list.extend(self._redeem_from_positions(positions, len(batch)))
|
|
226
|
+
return redeem_list
|
|
227
|
+
|
|
228
|
+
def _redeem_from_positions(
|
|
229
|
+
self, positions: list[dict], batch_size: int
|
|
230
|
+
) -> list[dict]:
|
|
231
|
+
"""
|
|
232
|
+
Build and submit redeem transactions from a list of positions.
|
|
233
|
+
"""
|
|
234
|
+
if not positions:
|
|
235
|
+
return []
|
|
236
|
+
positions_by_condition: dict[str, list[dict]] = {}
|
|
237
|
+
for pos in positions:
|
|
238
|
+
condition_id = pos.get("conditionId")
|
|
239
|
+
if not condition_id:
|
|
240
|
+
continue
|
|
241
|
+
positions_by_condition.setdefault(condition_id, []).append(pos)
|
|
242
|
+
|
|
243
|
+
redeem_list = []
|
|
244
|
+
error_list: list[str] = []
|
|
245
|
+
condition_ids = list(positions_by_condition.keys())
|
|
246
|
+
for batch in self._chunk_condition_ids(condition_ids, batch_size):
|
|
247
|
+
batch_positions = []
|
|
248
|
+
for condition_id in batch:
|
|
249
|
+
batch_positions.extend(positions_by_condition.get(condition_id, []))
|
|
250
|
+
try:
|
|
251
|
+
txs = self._build_redeem_txs_from_positions(batch_positions)
|
|
252
|
+
if not txs:
|
|
253
|
+
continue
|
|
254
|
+
redeem_res = self._submit_redeem(txs)
|
|
255
|
+
redeem_list.append(redeem_res)
|
|
256
|
+
for pos in batch_positions:
|
|
257
|
+
buy_price = pos.get("avgPrice")
|
|
258
|
+
size = pos.get("size")
|
|
259
|
+
if not buy_price or not size:
|
|
260
|
+
continue
|
|
261
|
+
volume = 1 / buy_price * (buy_price * size)
|
|
262
|
+
logger.info(
|
|
263
|
+
f"{pos.get('slug')} redeem success, volume={volume:.4f} usdc"
|
|
264
|
+
)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
error_list.extend(batch)
|
|
267
|
+
logger.info(f"redeem batch error, {batch=}, error={e}")
|
|
268
|
+
if error_list:
|
|
269
|
+
logger.warning(f"error redeem condition list, {error_list}")
|
|
270
|
+
return redeem_list
|
|
271
|
+
|
|
169
272
|
@classmethod
|
|
170
273
|
def _get_relay_payload(cls, address: str, wallet_type: WalletType):
|
|
171
274
|
return requests.get(
|
|
@@ -198,11 +301,32 @@ class BaseWeb3Service:
|
|
|
198
301
|
else:
|
|
199
302
|
raise Exception("Estimate gas error: " + str(result))
|
|
200
303
|
|
|
304
|
+
@classmethod
|
|
305
|
+
def _chunk_condition_ids(
|
|
306
|
+
cls, condition_ids: list[str], batch_size: int
|
|
307
|
+
) -> list[list[str]]:
|
|
308
|
+
if batch_size <= 0:
|
|
309
|
+
raise Exception("batch_size must be greater than 0")
|
|
310
|
+
return [
|
|
311
|
+
condition_ids[i: i + batch_size]
|
|
312
|
+
for i in range(0, len(condition_ids), batch_size)
|
|
313
|
+
]
|
|
314
|
+
|
|
201
315
|
def redeem(
|
|
202
316
|
self,
|
|
203
317
|
condition_ids: str | list[str],
|
|
204
|
-
|
|
205
|
-
|
|
318
|
+
batch_size: int = 10,
|
|
319
|
+
):
|
|
320
|
+
"""
|
|
321
|
+
Redeem positions for the given condition IDs.
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(condition_ids, str):
|
|
324
|
+
condition_ids = [condition_ids]
|
|
325
|
+
return self._redeem_batch(condition_ids, batch_size)
|
|
206
326
|
|
|
207
|
-
def redeem_all(self) -> list[dict]
|
|
208
|
-
|
|
327
|
+
def redeem_all(self, batch_size: int = 10) -> list[dict]:
|
|
328
|
+
"""
|
|
329
|
+
Redeem all currently redeemable positions for the user.
|
|
330
|
+
"""
|
|
331
|
+
positions = self.fetch_positions(user_address=self._resolve_user_address())
|
|
332
|
+
return self._redeem_from_positions(positions, batch_size)
|
|
@@ -10,8 +10,10 @@ from poly_web3.web3_service.base import BaseWeb3Service
|
|
|
10
10
|
class EOAWeb3Service(BaseWeb3Service):
|
|
11
11
|
def redeem(
|
|
12
12
|
self,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
redeem_amounts: list[int] | None = None,
|
|
13
|
+
condition_ids: str | list[str],
|
|
14
|
+
batch_size: int = 10,
|
|
16
15
|
):
|
|
17
16
|
raise ImportError("EOA wallet redeem not supported")
|
|
17
|
+
|
|
18
|
+
def redeem_all(self, batch_size: int = 10) -> list[dict]:
|
|
19
|
+
raise ImportError("EOA wallet redeem not supported")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding = utf-8 -*-
|
|
2
|
+
# @Time: 2025-12-27 16:00:42
|
|
3
|
+
# @Author: PinBar
|
|
4
|
+
# @Site:
|
|
5
|
+
# @File: proxy_service.py
|
|
6
|
+
# @Software: PyCharm
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from web3 import Web3
|
|
10
|
+
from eth_utils import to_bytes
|
|
11
|
+
|
|
12
|
+
from poly_web3.const import (
|
|
13
|
+
proxy_wallet_factory_abi,
|
|
14
|
+
RELAYER_URL,
|
|
15
|
+
PROXY_INIT_CODE_HASH,
|
|
16
|
+
SUBMIT_TRANSACTION,
|
|
17
|
+
STATE_MINED,
|
|
18
|
+
STATE_CONFIRMED,
|
|
19
|
+
STATE_FAILED,
|
|
20
|
+
)
|
|
21
|
+
from poly_web3.web3_service.base import BaseWeb3Service
|
|
22
|
+
from poly_web3.signature.build import derive_proxy_wallet, create_struct_hash
|
|
23
|
+
from poly_web3.signature.hash_message import hash_message
|
|
24
|
+
from poly_web3.signature import secp256k1
|
|
25
|
+
class ProxyWeb3Service(BaseWeb3Service):
|
|
26
|
+
def _build_redeem_tx(self, to: str, data: str) -> dict:
|
|
27
|
+
return {
|
|
28
|
+
"to": to,
|
|
29
|
+
"data": data,
|
|
30
|
+
"value": 0,
|
|
31
|
+
"typeCode": 1,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def build_proxy_transaction_request(self, args: dict) -> dict:
|
|
35
|
+
proxy_contract_config = self.get_contract_config()["ProxyContracts"]
|
|
36
|
+
to = proxy_contract_config["ProxyFactory"]
|
|
37
|
+
proxy = derive_proxy_wallet(args["from"], to, PROXY_INIT_CODE_HASH)
|
|
38
|
+
relayer_fee = "0"
|
|
39
|
+
relay_hub = proxy_contract_config["RelayHub"]
|
|
40
|
+
gas_limit_str = self.estimate_gas(
|
|
41
|
+
tx={"from": args["from"], "to": to, "data": args["data"]}
|
|
42
|
+
)
|
|
43
|
+
sig_params = {
|
|
44
|
+
"gasPrice": args.get("gasPrice"),
|
|
45
|
+
"gasLimit": gas_limit_str,
|
|
46
|
+
"relayerFee": relayer_fee,
|
|
47
|
+
"relayHub": relay_hub,
|
|
48
|
+
"relay": args.get("relay"),
|
|
49
|
+
}
|
|
50
|
+
tx_hash = create_struct_hash(
|
|
51
|
+
args["from"],
|
|
52
|
+
to,
|
|
53
|
+
args["data"],
|
|
54
|
+
relayer_fee,
|
|
55
|
+
args.get("gasPrice"),
|
|
56
|
+
gas_limit_str,
|
|
57
|
+
args["nonce"],
|
|
58
|
+
relay_hub,
|
|
59
|
+
args.get("relay"),
|
|
60
|
+
)
|
|
61
|
+
message = {"raw": list(to_bytes(hexstr=tx_hash))}
|
|
62
|
+
|
|
63
|
+
r, s, recovery = secp256k1.sign(
|
|
64
|
+
hash_message(message)[2:], self.clob_client.signer.private_key
|
|
65
|
+
)
|
|
66
|
+
signature = {
|
|
67
|
+
"r": secp256k1.int_to_hex(r, 32),
|
|
68
|
+
"s": secp256k1.int_to_hex(s, 32),
|
|
69
|
+
"v": 28 if recovery else 27,
|
|
70
|
+
"yParity": recovery,
|
|
71
|
+
}
|
|
72
|
+
final_sig = secp256k1.serialize_signature(**signature, to="hex")
|
|
73
|
+
req = {
|
|
74
|
+
"from": args["from"],
|
|
75
|
+
"to": to,
|
|
76
|
+
"proxyWallet": proxy,
|
|
77
|
+
"data": args["data"],
|
|
78
|
+
"nonce": args["nonce"],
|
|
79
|
+
"signature": final_sig,
|
|
80
|
+
"signatureParams": sig_params,
|
|
81
|
+
"type": self.wallet_type.value,
|
|
82
|
+
"metadata": "redeem",
|
|
83
|
+
}
|
|
84
|
+
return req
|
|
85
|
+
|
|
86
|
+
def encode_proxy_transaction_data(self, txns):
|
|
87
|
+
# Prepare the arguments for the 'proxy' function
|
|
88
|
+
calls_data = [
|
|
89
|
+
(txn["typeCode"], txn["to"], txn["value"], txn["data"]) for txn in txns
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Create the contract object
|
|
93
|
+
contract = self.w3.eth.contract(abi=proxy_wallet_factory_abi)
|
|
94
|
+
|
|
95
|
+
# Encode function data
|
|
96
|
+
function_data = contract.encodeABI(fn_name="proxy", args=[calls_data])
|
|
97
|
+
|
|
98
|
+
return function_data
|
|
99
|
+
|
|
100
|
+
def _submit_redeem(self, txs: list[dict]) -> dict:
|
|
101
|
+
if self.clob_client is None:
|
|
102
|
+
raise Exception("signer not found")
|
|
103
|
+
_from = Web3.to_checksum_address(self.clob_client.get_address())
|
|
104
|
+
rp = self._get_relay_payload(_from, self.wallet_type)
|
|
105
|
+
args = {
|
|
106
|
+
"from": _from,
|
|
107
|
+
"gasPrice": "0",
|
|
108
|
+
"data": self.encode_proxy_transaction_data(txs),
|
|
109
|
+
"relay": rp["address"],
|
|
110
|
+
"nonce": rp["nonce"],
|
|
111
|
+
}
|
|
112
|
+
req = self.build_proxy_transaction_request(args)
|
|
113
|
+
headers = self.relayer_client._generate_builder_headers(
|
|
114
|
+
"POST", SUBMIT_TRANSACTION, req
|
|
115
|
+
)
|
|
116
|
+
response = requests.post(
|
|
117
|
+
RELAYER_URL + SUBMIT_TRANSACTION, json=req, headers=headers
|
|
118
|
+
).json()
|
|
119
|
+
redeem_res = self.relayer_client.poll_until_state(
|
|
120
|
+
transaction_id=response["transactionID"],
|
|
121
|
+
states=[STATE_MINED, STATE_CONFIRMED],
|
|
122
|
+
fail_state=STATE_FAILED,
|
|
123
|
+
max_polls=100,
|
|
124
|
+
)
|
|
125
|
+
return redeem_res
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding = utf-8 -*-
|
|
2
|
+
# @Time: 2025-12-27 16:01:00
|
|
3
|
+
# @Author: PinBar
|
|
4
|
+
# @Site:
|
|
5
|
+
# @File: safe_service.py
|
|
6
|
+
# @Software: PyCharm
|
|
7
|
+
from py_builder_relayer_client.models import OperationType, SafeTransaction
|
|
8
|
+
from poly_web3.web3_service.base import BaseWeb3Service
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SafeWeb3Service(BaseWeb3Service):
|
|
12
|
+
def _build_redeem_tx(self, to: str, data: str) -> SafeTransaction:
|
|
13
|
+
return SafeTransaction(
|
|
14
|
+
to=to,
|
|
15
|
+
data=data,
|
|
16
|
+
value="0",
|
|
17
|
+
operation=OperationType.Call,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def _submit_redeem(self, txs: list[SafeTransaction]) -> dict | None:
|
|
21
|
+
if self.relayer_client is None:
|
|
22
|
+
raise Exception("relayer_client not found")
|
|
23
|
+
resp = self.relayer_client.execute(txs, "redeem")
|
|
24
|
+
return resp.wait()
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
# -*- coding = utf-8 -*-
|
|
2
|
-
# @Time: 2025-12-27 16:00:42
|
|
3
|
-
# @Author: PinBar
|
|
4
|
-
# @Site:
|
|
5
|
-
# @File: proxy_service.py
|
|
6
|
-
# @Software: PyCharm
|
|
7
|
-
import requests
|
|
8
|
-
|
|
9
|
-
from web3 import Web3
|
|
10
|
-
from eth_utils import to_bytes
|
|
11
|
-
|
|
12
|
-
from poly_web3.const import (
|
|
13
|
-
proxy_wallet_factory_abi,
|
|
14
|
-
CTF_ADDRESS,
|
|
15
|
-
RELAYER_URL,
|
|
16
|
-
PROXY_INIT_CODE_HASH,
|
|
17
|
-
SUBMIT_TRANSACTION,
|
|
18
|
-
STATE_MINED,
|
|
19
|
-
STATE_CONFIRMED,
|
|
20
|
-
STATE_FAILED,
|
|
21
|
-
NEG_RISK_ADAPTER_ADDRESS,
|
|
22
|
-
)
|
|
23
|
-
from poly_web3.web3_service.base import BaseWeb3Service
|
|
24
|
-
from poly_web3.signature.build import derive_proxy_wallet, create_struct_hash
|
|
25
|
-
from poly_web3.signature.hash_message import hash_message
|
|
26
|
-
from poly_web3.signature import secp256k1
|
|
27
|
-
from poly_web3.log import logger
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ProxyWeb3Service(BaseWeb3Service):
|
|
31
|
-
def build_proxy_transaction_request(self, args: dict) -> dict:
|
|
32
|
-
proxy_contract_config = self.get_contract_config()["ProxyContracts"]
|
|
33
|
-
to = proxy_contract_config["ProxyFactory"]
|
|
34
|
-
proxy = derive_proxy_wallet(args["from"], to, PROXY_INIT_CODE_HASH)
|
|
35
|
-
relayer_fee = "0"
|
|
36
|
-
relay_hub = proxy_contract_config["RelayHub"]
|
|
37
|
-
gas_limit_str = self.estimate_gas(
|
|
38
|
-
tx={"from": args["from"], "to": to, "data": args["data"]}
|
|
39
|
-
)
|
|
40
|
-
sig_params = {
|
|
41
|
-
"gasPrice": args.get("gasPrice"),
|
|
42
|
-
"gasLimit": gas_limit_str,
|
|
43
|
-
"relayerFee": relayer_fee,
|
|
44
|
-
"relayHub": relay_hub,
|
|
45
|
-
"relay": args.get("relay"),
|
|
46
|
-
}
|
|
47
|
-
tx_hash = create_struct_hash(
|
|
48
|
-
args["from"],
|
|
49
|
-
to,
|
|
50
|
-
args["data"],
|
|
51
|
-
relayer_fee,
|
|
52
|
-
args.get("gasPrice"),
|
|
53
|
-
gas_limit_str,
|
|
54
|
-
args["nonce"],
|
|
55
|
-
relay_hub,
|
|
56
|
-
args.get("relay"),
|
|
57
|
-
)
|
|
58
|
-
message = {"raw": list(to_bytes(hexstr=tx_hash))}
|
|
59
|
-
|
|
60
|
-
r, s, recovery = secp256k1.sign(
|
|
61
|
-
hash_message(message)[2:], self.clob_client.signer.private_key
|
|
62
|
-
)
|
|
63
|
-
signature = {
|
|
64
|
-
"r": secp256k1.int_to_hex(r, 32),
|
|
65
|
-
"s": secp256k1.int_to_hex(s, 32),
|
|
66
|
-
"v": 28 if recovery else 27,
|
|
67
|
-
"yParity": recovery,
|
|
68
|
-
}
|
|
69
|
-
final_sig = secp256k1.serialize_signature(**signature, to="hex")
|
|
70
|
-
req = {
|
|
71
|
-
"from": args["from"],
|
|
72
|
-
"to": to,
|
|
73
|
-
"proxyWallet": proxy,
|
|
74
|
-
"data": args["data"],
|
|
75
|
-
"nonce": args["nonce"],
|
|
76
|
-
"signature": final_sig,
|
|
77
|
-
"signatureParams": sig_params,
|
|
78
|
-
"type": self.wallet_type.value,
|
|
79
|
-
"metadata": "redeem",
|
|
80
|
-
}
|
|
81
|
-
return req
|
|
82
|
-
|
|
83
|
-
def encode_proxy_transaction_data(self, txns):
|
|
84
|
-
# Prepare the arguments for the 'proxy' function
|
|
85
|
-
calls_data = [
|
|
86
|
-
(txn["typeCode"], txn["to"], txn["value"], txn["data"]) for txn in txns
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
# Create the contract object
|
|
90
|
-
contract = self.w3.eth.contract(abi=proxy_wallet_factory_abi)
|
|
91
|
-
|
|
92
|
-
# Encode function data
|
|
93
|
-
function_data = contract.encodeABI(fn_name="proxy", args=[calls_data])
|
|
94
|
-
|
|
95
|
-
return function_data
|
|
96
|
-
|
|
97
|
-
def _build_redeem_txs_from_positions(self, positions: list[dict]) -> list[dict]:
|
|
98
|
-
neg_amounts_by_condition: dict[str, list[float]] = {}
|
|
99
|
-
normal_conditions: set[str] = set()
|
|
100
|
-
for pos in positions:
|
|
101
|
-
condition_id = pos.get("conditionId")
|
|
102
|
-
if not condition_id:
|
|
103
|
-
continue
|
|
104
|
-
if pos.get("negativeRisk"):
|
|
105
|
-
idx = pos.get("outcomeIndex")
|
|
106
|
-
size = pos.get("size")
|
|
107
|
-
if idx is None or size is None:
|
|
108
|
-
continue
|
|
109
|
-
amounts = neg_amounts_by_condition.setdefault(condition_id, [0.0, 0.0])
|
|
110
|
-
if idx not in (0, 1):
|
|
111
|
-
raise Exception(f"negRisk outcomeIndex out of range: {idx}")
|
|
112
|
-
amounts[idx] += size
|
|
113
|
-
else:
|
|
114
|
-
normal_conditions.add(condition_id)
|
|
115
|
-
txs: list[dict] = []
|
|
116
|
-
for condition_id, amounts in neg_amounts_by_condition.items():
|
|
117
|
-
int_amounts = [int(amount * 1e6) for amount in amounts]
|
|
118
|
-
txs.append(
|
|
119
|
-
{
|
|
120
|
-
"to": NEG_RISK_ADAPTER_ADDRESS,
|
|
121
|
-
"data": self.build_neg_risk_redeem_tx_data(condition_id, int_amounts),
|
|
122
|
-
"value": 0,
|
|
123
|
-
"typeCode": 1,
|
|
124
|
-
}
|
|
125
|
-
)
|
|
126
|
-
for condition_id in normal_conditions:
|
|
127
|
-
txs.append(
|
|
128
|
-
{
|
|
129
|
-
"to": CTF_ADDRESS,
|
|
130
|
-
"data": self.build_ctf_redeem_tx_data(condition_id),
|
|
131
|
-
"value": 0,
|
|
132
|
-
"typeCode": 1,
|
|
133
|
-
}
|
|
134
|
-
)
|
|
135
|
-
return txs
|
|
136
|
-
|
|
137
|
-
@classmethod
|
|
138
|
-
def _chunk_condition_ids(
|
|
139
|
-
cls, condition_ids: list[str], batch_size: int
|
|
140
|
-
) -> list[list[str]]:
|
|
141
|
-
if batch_size <= 0:
|
|
142
|
-
raise Exception("batch_size must be greater than 0")
|
|
143
|
-
return [
|
|
144
|
-
condition_ids[i: i + batch_size]
|
|
145
|
-
for i in range(0, len(condition_ids), batch_size)
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
def _redeem_batch(self, condition_ids: list[str], batch_size: int) -> list[dict]:
|
|
149
|
-
if not condition_ids:
|
|
150
|
-
return []
|
|
151
|
-
user_address = self._resolve_user_address()
|
|
152
|
-
redeem_list = []
|
|
153
|
-
for batch in self._chunk_condition_ids(condition_ids, batch_size):
|
|
154
|
-
positions = self.fetch_positions_by_condition_ids(
|
|
155
|
-
user_address=user_address, condition_ids=batch
|
|
156
|
-
)
|
|
157
|
-
redeem_list.extend(self._redeem_from_positions(positions, len(batch)))
|
|
158
|
-
return redeem_list
|
|
159
|
-
|
|
160
|
-
def _submit_proxy_redeem(self, txs: list[dict]) -> dict:
|
|
161
|
-
if self.clob_client is None:
|
|
162
|
-
raise Exception("signer not found")
|
|
163
|
-
_from = Web3.to_checksum_address(self.clob_client.get_address())
|
|
164
|
-
rp = self._get_relay_payload(_from, self.wallet_type)
|
|
165
|
-
args = {
|
|
166
|
-
"from": _from,
|
|
167
|
-
"gasPrice": "0",
|
|
168
|
-
"data": self.encode_proxy_transaction_data(txs),
|
|
169
|
-
"relay": rp["address"],
|
|
170
|
-
"nonce": rp["nonce"],
|
|
171
|
-
}
|
|
172
|
-
req = self.build_proxy_transaction_request(args)
|
|
173
|
-
headers = self.relayer_client._generate_builder_headers(
|
|
174
|
-
"POST", SUBMIT_TRANSACTION, req
|
|
175
|
-
)
|
|
176
|
-
response = requests.post(
|
|
177
|
-
RELAYER_URL + SUBMIT_TRANSACTION, json=req, headers=headers
|
|
178
|
-
).json()
|
|
179
|
-
redeem_res = self.relayer_client.poll_until_state(
|
|
180
|
-
transaction_id=response["transactionID"],
|
|
181
|
-
states=[STATE_MINED, STATE_CONFIRMED],
|
|
182
|
-
fail_state=STATE_FAILED,
|
|
183
|
-
max_polls=100,
|
|
184
|
-
)
|
|
185
|
-
return redeem_res
|
|
186
|
-
|
|
187
|
-
def _redeem_from_positions(
|
|
188
|
-
self, positions: list[dict], batch_size: int
|
|
189
|
-
) -> list[dict]:
|
|
190
|
-
if not positions:
|
|
191
|
-
return []
|
|
192
|
-
positions_by_condition: dict[str, list[dict]] = {}
|
|
193
|
-
for pos in positions:
|
|
194
|
-
condition_id = pos.get("conditionId")
|
|
195
|
-
if not condition_id:
|
|
196
|
-
continue
|
|
197
|
-
positions_by_condition.setdefault(condition_id, []).append(pos)
|
|
198
|
-
|
|
199
|
-
redeem_list = []
|
|
200
|
-
condition_ids = list(positions_by_condition.keys())
|
|
201
|
-
for batch in self._chunk_condition_ids(condition_ids, batch_size):
|
|
202
|
-
batch_positions = []
|
|
203
|
-
for condition_id in batch:
|
|
204
|
-
batch_positions.extend(positions_by_condition.get(condition_id, []))
|
|
205
|
-
try:
|
|
206
|
-
txs = self._build_redeem_txs_from_positions(batch_positions)
|
|
207
|
-
if not txs:
|
|
208
|
-
continue
|
|
209
|
-
redeem_res = self._submit_proxy_redeem(txs)
|
|
210
|
-
redeem_list.append(redeem_res)
|
|
211
|
-
for pos in batch_positions:
|
|
212
|
-
buy_price = pos.get("avgPrice")
|
|
213
|
-
size = pos.get("size")
|
|
214
|
-
if not buy_price or not size:
|
|
215
|
-
continue
|
|
216
|
-
volume = 1 / buy_price * (buy_price * size)
|
|
217
|
-
logger.info(
|
|
218
|
-
f"{pos.get('slug')} redeem success, volume={volume:.4f} usdc"
|
|
219
|
-
)
|
|
220
|
-
except Exception as e:
|
|
221
|
-
logger.info(f"redeem batch error, {batch=}, error={e}")
|
|
222
|
-
return redeem_list
|
|
223
|
-
|
|
224
|
-
def redeem(
|
|
225
|
-
self,
|
|
226
|
-
condition_ids: str | list[str],
|
|
227
|
-
batch_size: int = 10,
|
|
228
|
-
):
|
|
229
|
-
if isinstance(condition_ids, str):
|
|
230
|
-
condition_ids = [condition_ids]
|
|
231
|
-
return self._redeem_batch(condition_ids, batch_size)
|
|
232
|
-
|
|
233
|
-
def redeem_all(self, batch_size: int = 10) -> list[dict]:
|
|
234
|
-
positions = self.fetch_positions(user_address=self._resolve_user_address())
|
|
235
|
-
return self._redeem_from_positions(positions, batch_size)
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# -*- coding = utf-8 -*-
|
|
2
|
-
# @Time: 2025-12-27 16:01:00
|
|
3
|
-
# @Author: PinBar
|
|
4
|
-
# @Site:
|
|
5
|
-
# @File: safe_service.py
|
|
6
|
-
# @Software: PyCharm
|
|
7
|
-
from poly_web3.web3_service.base import BaseWeb3Service
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SafeWeb3Service(BaseWeb3Service):
|
|
11
|
-
def redeem(
|
|
12
|
-
self,
|
|
13
|
-
condition_id: str,
|
|
14
|
-
neg_risk: bool = False,
|
|
15
|
-
redeem_amounts: list[int] | None = None,
|
|
16
|
-
):
|
|
17
|
-
raise ImportError("Safe wallet redeem not supported")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|