poly-web3 0.0.3__py3-none-any.whl → 0.0.5__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.
- examples/example_redeem.py +67 -63
- poly_web3/__init__.py +33 -33
- poly_web3/const.py +171 -171
- poly_web3/log.py +48 -18
- poly_web3/schema.py +22 -22
- poly_web3/signature/__init__.py +6 -6
- poly_web3/signature/build.py +113 -113
- poly_web3/signature/hash_message.py +48 -48
- poly_web3/signature/secp256k1.py +57 -57
- poly_web3/web3_service/__init__.py +9 -9
- poly_web3/web3_service/base.py +208 -182
- poly_web3/web3_service/eoa_service.py +17 -17
- poly_web3/web3_service/proxy_service.py +235 -166
- poly_web3/web3_service/safe_service.py +17 -17
- {poly_web3-0.0.3.dist-info → poly_web3-0.0.5.dist-info}/METADATA +298 -302
- poly_web3-0.0.5.dist-info/RECORD +18 -0
- poly_web3-0.0.3.dist-info/RECORD +0 -18
- {poly_web3-0.0.3.dist-info → poly_web3-0.0.5.dist-info}/WHEEL +0 -0
- {poly_web3-0.0.3.dist-info → poly_web3-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -1,166 +1,235 @@
|
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 +1,17 @@
|
|
|
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")
|
|
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")
|