poly-web3 0.0.1__py3-none-any.whl → 0.0.2__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 +63 -54
- poly_web3/__init__.py +33 -31
- poly_web3/const.py +171 -171
- 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 +181 -143
- poly_web3/web3_service/eoa_service.py +17 -17
- poly_web3/web3_service/proxy_service.py +163 -135
- poly_web3/web3_service/safe_service.py +17 -17
- {poly_web3-0.0.1.dist-info → poly_web3-0.0.2.dist-info}/METADATA +302 -275
- poly_web3-0.0.2.dist-info/RECORD +17 -0
- poly_web3-0.0.1.dist-info/RECORD +0 -17
- {poly_web3-0.0.1.dist-info → poly_web3-0.0.2.dist-info}/WHEEL +0 -0
- {poly_web3-0.0.1.dist-info → poly_web3-0.0.2.dist-info}/top_level.txt +0 -0
poly_web3/web3_service/base.py
CHANGED
|
@@ -1,143 +1,181 @@
|
|
|
1
|
-
# -*- coding = utf-8 -*-
|
|
2
|
-
# @Time: 2025-12-27 15:57:07
|
|
3
|
-
# @Author: PinBar
|
|
4
|
-
# @Site:
|
|
5
|
-
# @File: base.py
|
|
6
|
-
# @Software: PyCharm
|
|
7
|
-
import requests
|
|
8
|
-
from py_builder_relayer_client.client import RelayClient
|
|
9
|
-
from py_clob_client.client import ClobClient
|
|
10
|
-
from web3 import Web3
|
|
11
|
-
|
|
12
|
-
from poly_web3.const import (
|
|
13
|
-
RPC_URL,
|
|
14
|
-
CTF_ADDRESS,
|
|
15
|
-
CTF_ABI_PAYOUT,
|
|
16
|
-
ZERO_BYTES32,
|
|
17
|
-
USDC_POLYGON,
|
|
18
|
-
CTF_ABI_REDEEM,
|
|
19
|
-
NEG_RISK_ADAPTER_ADDRESS,
|
|
20
|
-
RELAYER_URL,
|
|
21
|
-
POL,
|
|
22
|
-
AMOY,
|
|
23
|
-
GET_RELAY_PAYLOAD,
|
|
24
|
-
NEG_RISK_ADAPTER_ABI_REDEEM,
|
|
25
|
-
)
|
|
26
|
-
from poly_web3.schema import WalletType
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class BaseWeb3Service:
|
|
30
|
-
def __init__(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
# -*- coding = utf-8 -*-
|
|
2
|
+
# @Time: 2025-12-27 15:57:07
|
|
3
|
+
# @Author: PinBar
|
|
4
|
+
# @Site:
|
|
5
|
+
# @File: base.py
|
|
6
|
+
# @Software: PyCharm
|
|
7
|
+
import requests
|
|
8
|
+
from py_builder_relayer_client.client import RelayClient
|
|
9
|
+
from py_clob_client.client import ClobClient
|
|
10
|
+
from web3 import Web3
|
|
11
|
+
|
|
12
|
+
from poly_web3.const import (
|
|
13
|
+
RPC_URL,
|
|
14
|
+
CTF_ADDRESS,
|
|
15
|
+
CTF_ABI_PAYOUT,
|
|
16
|
+
ZERO_BYTES32,
|
|
17
|
+
USDC_POLYGON,
|
|
18
|
+
CTF_ABI_REDEEM,
|
|
19
|
+
NEG_RISK_ADAPTER_ADDRESS,
|
|
20
|
+
RELAYER_URL,
|
|
21
|
+
POL,
|
|
22
|
+
AMOY,
|
|
23
|
+
GET_RELAY_PAYLOAD,
|
|
24
|
+
NEG_RISK_ADAPTER_ABI_REDEEM,
|
|
25
|
+
)
|
|
26
|
+
from poly_web3.schema import WalletType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseWeb3Service:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
clob_client: ClobClient = None,
|
|
33
|
+
relayer_client: RelayClient = None,
|
|
34
|
+
rpc_url: str | None = None,
|
|
35
|
+
):
|
|
36
|
+
self.relayer_client = relayer_client
|
|
37
|
+
self.clob_client: ClobClient = clob_client
|
|
38
|
+
if self.clob_client:
|
|
39
|
+
self.wallet_type: WalletType = WalletType.get_with_code(
|
|
40
|
+
self.clob_client.builder.sig_type
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
self.wallet_type = WalletType.PROXY
|
|
44
|
+
self.rpc_url = rpc_url or RPC_URL
|
|
45
|
+
self.w3: Web3 = Web3(Web3.HTTPProvider(self.rpc_url))
|
|
46
|
+
if self.wallet_type == WalletType.PROXY and relayer_client is None:
|
|
47
|
+
raise Exception("relayer_client must be provided")
|
|
48
|
+
|
|
49
|
+
def _resolve_user_address(self):
|
|
50
|
+
funder = getattr(getattr(self.clob_client, "builder", None), "funder", None)
|
|
51
|
+
if funder:
|
|
52
|
+
return funder
|
|
53
|
+
return self.clob_client.get_address()
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def fetch_positions(cls, user_address: str) -> list[dict]:
|
|
57
|
+
"""
|
|
58
|
+
Fetches current positions for a user from the official Polymarket API.
|
|
59
|
+
|
|
60
|
+
:param user_address: User wallet address (0x-prefixed, 40 hex chars)
|
|
61
|
+
:return: List of position dictionaries from the API
|
|
62
|
+
"""
|
|
63
|
+
url = "https://data-api.polymarket.com/positions"
|
|
64
|
+
params = {
|
|
65
|
+
"user": user_address,
|
|
66
|
+
"sizeThreshold": 1,
|
|
67
|
+
"limit": 100,
|
|
68
|
+
"redeemable": True,
|
|
69
|
+
"sortBy": "RESOLVING",
|
|
70
|
+
"sortDirection": "DESC",
|
|
71
|
+
}
|
|
72
|
+
try:
|
|
73
|
+
response = requests.get(url, params=params)
|
|
74
|
+
response.raise_for_status()
|
|
75
|
+
positions = response.json()
|
|
76
|
+
return [i for i in positions if i.get("percentPnl") > 0]
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Failed to fetch positions from API: {e}")
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def is_condition_resolved(self, condition_id: str) -> bool:
|
|
82
|
+
ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
|
|
83
|
+
return ctf.functions.payoutDenominator(condition_id).call() > 0
|
|
84
|
+
|
|
85
|
+
def get_winning_indexes(self, condition_id: str) -> list[int]:
|
|
86
|
+
ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
|
|
87
|
+
if not self.is_condition_resolved(condition_id):
|
|
88
|
+
return []
|
|
89
|
+
outcome_count = ctf.functions.getOutcomeSlotCount(condition_id).call()
|
|
90
|
+
winners: list[int] = []
|
|
91
|
+
for i in range(outcome_count):
|
|
92
|
+
if ctf.functions.payoutNumerators(condition_id, i).call() > 0:
|
|
93
|
+
winners.append(i)
|
|
94
|
+
return winners
|
|
95
|
+
|
|
96
|
+
def get_redeemable_index_and_balance(
|
|
97
|
+
self, condition_id: str
|
|
98
|
+
) -> list[tuple]:
|
|
99
|
+
owner = self._resolve_user_address()
|
|
100
|
+
winners = self.get_winning_indexes(condition_id)
|
|
101
|
+
if not winners:
|
|
102
|
+
return []
|
|
103
|
+
ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
|
|
104
|
+
owner_checksum = Web3.to_checksum_address(owner)
|
|
105
|
+
redeemable: list[tuple] = []
|
|
106
|
+
for index in winners:
|
|
107
|
+
index_set = 1 << index
|
|
108
|
+
collection_id = ctf.functions.getCollectionId(
|
|
109
|
+
ZERO_BYTES32, condition_id, index_set
|
|
110
|
+
).call()
|
|
111
|
+
position_id = ctf.functions.getPositionId(
|
|
112
|
+
USDC_POLYGON, collection_id
|
|
113
|
+
).call()
|
|
114
|
+
balance = ctf.functions.balanceOf(owner_checksum, position_id).call()
|
|
115
|
+
if balance > 0:
|
|
116
|
+
redeemable.append((index, balance / 1000000))
|
|
117
|
+
return redeemable
|
|
118
|
+
|
|
119
|
+
def build_ctf_redeem_tx_data(self, condition_id: str) -> str:
|
|
120
|
+
ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_REDEEM)
|
|
121
|
+
# 只需要 calldata:encodeABI 即可
|
|
122
|
+
return ctf.functions.redeemPositions(
|
|
123
|
+
USDC_POLYGON,
|
|
124
|
+
ZERO_BYTES32,
|
|
125
|
+
condition_id,
|
|
126
|
+
[1, 2],
|
|
127
|
+
)._encode_transaction_data()
|
|
128
|
+
|
|
129
|
+
def build_neg_risk_redeem_tx_data(
|
|
130
|
+
self, condition_id: str, redeem_amounts: list[int]
|
|
131
|
+
) -> str:
|
|
132
|
+
nr_adapter = self.w3.eth.contract(
|
|
133
|
+
address=NEG_RISK_ADAPTER_ADDRESS, abi=NEG_RISK_ADAPTER_ABI_REDEEM
|
|
134
|
+
)
|
|
135
|
+
return nr_adapter.functions.redeemPositions(
|
|
136
|
+
condition_id,
|
|
137
|
+
redeem_amounts,
|
|
138
|
+
)._encode_transaction_data()
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _get_relay_payload(cls, address: str, wallet_type: WalletType):
|
|
142
|
+
return requests.get(
|
|
143
|
+
RELAYER_URL + GET_RELAY_PAYLOAD,
|
|
144
|
+
params={"address": address, "type": wallet_type},
|
|
145
|
+
).json()
|
|
146
|
+
|
|
147
|
+
def get_contract_config(self) -> dict:
|
|
148
|
+
if self.clob_client.chain_id == 137:
|
|
149
|
+
return POL
|
|
150
|
+
elif self.clob_client.chain_id == 80002:
|
|
151
|
+
return AMOY
|
|
152
|
+
raise Exception("Invalid network")
|
|
153
|
+
|
|
154
|
+
def estimate_gas(self, tx):
|
|
155
|
+
payload = {
|
|
156
|
+
"jsonrpc": "2.0",
|
|
157
|
+
"method": "eth_estimateGas",
|
|
158
|
+
"params": [tx],
|
|
159
|
+
"id": 1,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
response = requests.post(self.rpc_url, json=payload)
|
|
163
|
+
result = response.json()
|
|
164
|
+
|
|
165
|
+
if "result" in result:
|
|
166
|
+
# 返回的是16进制 gas 数量
|
|
167
|
+
gas_hex = result["result"]
|
|
168
|
+
return str(int(gas_hex, 16))
|
|
169
|
+
else:
|
|
170
|
+
raise Exception("Estimate gas error: " + str(result))
|
|
171
|
+
|
|
172
|
+
def redeem(
|
|
173
|
+
self,
|
|
174
|
+
condition_id: str,
|
|
175
|
+
neg_risk: bool = False,
|
|
176
|
+
redeem_amounts: list[int] | None = None,
|
|
177
|
+
): # noqa:
|
|
178
|
+
raise ImportError()
|
|
179
|
+
|
|
180
|
+
def redeem_all(self) -> list[dict] | None:
|
|
181
|
+
raise ImportError()
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# -*- coding = utf-8 -*-
|
|
2
|
-
# @Time: 2025-12-27 16:01:09
|
|
3
|
-
# @Author: PinBar
|
|
4
|
-
# @Site:
|
|
5
|
-
# @File: eoa_service.py
|
|
6
|
-
# @Software: PyCharm
|
|
7
|
-
from poly_web3.web3_service.base import BaseWeb3Service
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class EOAWeb3Service(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("EOA wallet redeem not supported")
|
|
1
|
+
# -*- coding = utf-8 -*-
|
|
2
|
+
# @Time: 2025-12-27 16:01:09
|
|
3
|
+
# @Author: PinBar
|
|
4
|
+
# @Site:
|
|
5
|
+
# @File: eoa_service.py
|
|
6
|
+
# @Software: PyCharm
|
|
7
|
+
from poly_web3.web3_service.base import BaseWeb3Service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EOAWeb3Service(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("EOA wallet redeem not supported")
|
|
@@ -1,135 +1,163 @@
|
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
class ProxyWeb3Service(BaseWeb3Service):
|
|
30
|
-
def build_proxy_transaction_request(self, args: dict) -> dict:
|
|
31
|
-
proxy_contract_config = self.get_contract_config()["ProxyContracts"]
|
|
32
|
-
to = proxy_contract_config["ProxyFactory"]
|
|
33
|
-
proxy = derive_proxy_wallet(args["from"], to, PROXY_INIT_CODE_HASH)
|
|
34
|
-
relayer_fee = "0"
|
|
35
|
-
relay_hub = proxy_contract_config["RelayHub"]
|
|
36
|
-
gas_limit_str = self.estimate_gas(
|
|
37
|
-
tx={"from": args["from"], "to": to, "data": args["data"]}
|
|
38
|
-
)
|
|
39
|
-
sig_params = {
|
|
40
|
-
"gasPrice": args.get("gasPrice"),
|
|
41
|
-
"gasLimit": gas_limit_str,
|
|
42
|
-
"relayerFee": relayer_fee,
|
|
43
|
-
"relayHub": relay_hub,
|
|
44
|
-
"relay": args.get("relay"),
|
|
45
|
-
}
|
|
46
|
-
tx_hash = create_struct_hash(
|
|
47
|
-
args["from"],
|
|
48
|
-
to,
|
|
49
|
-
args["data"],
|
|
50
|
-
relayer_fee,
|
|
51
|
-
args.get("gasPrice"),
|
|
52
|
-
gas_limit_str,
|
|
53
|
-
args["nonce"],
|
|
54
|
-
relay_hub,
|
|
55
|
-
args.get("relay"),
|
|
56
|
-
)
|
|
57
|
-
message = {"raw": list(to_bytes(hexstr=tx_hash))}
|
|
58
|
-
|
|
59
|
-
r, s, recovery = secp256k1.sign(
|
|
60
|
-
hash_message(message)[2:], self.clob_client.signer.private_key
|
|
61
|
-
)
|
|
62
|
-
signature = {
|
|
63
|
-
"r": secp256k1.int_to_hex(r, 32),
|
|
64
|
-
"s": secp256k1.int_to_hex(s, 32),
|
|
65
|
-
"v": 28 if recovery else 27,
|
|
66
|
-
"yParity": recovery,
|
|
67
|
-
}
|
|
68
|
-
final_sig = secp256k1.serialize_signature(**signature, to="hex")
|
|
69
|
-
req = {
|
|
70
|
-
"from": args["from"],
|
|
71
|
-
"to": to,
|
|
72
|
-
"proxyWallet": proxy,
|
|
73
|
-
"data": args["data"],
|
|
74
|
-
"nonce": args["nonce"],
|
|
75
|
-
"signature": final_sig,
|
|
76
|
-
"signatureParams": sig_params,
|
|
77
|
-
"type": self.wallet_type.value,
|
|
78
|
-
"metadata": "redeem",
|
|
79
|
-
}
|
|
80
|
-
return req
|
|
81
|
-
|
|
82
|
-
def encode_proxy_transaction_data(self, txns):
|
|
83
|
-
# Prepare the arguments for the 'proxy' function
|
|
84
|
-
calls_data = [
|
|
85
|
-
(txn["typeCode"], txn["to"], txn["value"], txn["data"]) for txn in txns
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
# Create the contract object
|
|
89
|
-
contract = self.w3.eth.contract(abi=proxy_wallet_factory_abi)
|
|
90
|
-
|
|
91
|
-
# Encode function data
|
|
92
|
-
function_data = contract.encodeABI(fn_name="proxy", args=[calls_data])
|
|
93
|
-
|
|
94
|
-
return function_data
|
|
95
|
-
|
|
96
|
-
def redeem(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
):
|
|
102
|
-
if neg_risk:
|
|
103
|
-
if redeem_amounts is None or len(redeem_amounts) != 2:
|
|
104
|
-
raise Exception("negRisk redeem requires redeem_amounts with length 2")
|
|
105
|
-
tx_data = self.build_neg_risk_redeem_tx_data(condition_id, redeem_amounts)
|
|
106
|
-
tx_to = NEG_RISK_ADAPTER_ADDRESS
|
|
107
|
-
else:
|
|
108
|
-
tx_data = self.build_ctf_redeem_tx_data(condition_id)
|
|
109
|
-
tx_to = CTF_ADDRESS
|
|
110
|
-
tx = {"to": tx_to, "data": tx_data, "value": 0, "typeCode": 1}
|
|
111
|
-
if self.clob_client is None:
|
|
112
|
-
raise Exception("signer not found")
|
|
113
|
-
_from = Web3.to_checksum_address(self.clob_client.get_address())
|
|
114
|
-
rp = self._get_relay_payload(_from, self.wallet_type)
|
|
115
|
-
args = {
|
|
116
|
-
"from": _from,
|
|
117
|
-
"gasPrice": "0",
|
|
118
|
-
"data": self.encode_proxy_transaction_data([tx]),
|
|
119
|
-
"relay": rp["address"],
|
|
120
|
-
"nonce": rp["nonce"],
|
|
121
|
-
}
|
|
122
|
-
req = self.build_proxy_transaction_request(args)
|
|
123
|
-
headers = self.relayer_client._generate_builder_headers(
|
|
124
|
-
"POST", SUBMIT_TRANSACTION, req
|
|
125
|
-
)
|
|
126
|
-
response = requests.post(
|
|
127
|
-
RELAYER_URL + SUBMIT_TRANSACTION, json=req, headers=headers
|
|
128
|
-
).json()
|
|
129
|
-
redeem_res = self.relayer_client.poll_until_state(
|
|
130
|
-
transaction_id=response["transactionID"],
|
|
131
|
-
states=[STATE_MINED, STATE_CONFIRMED],
|
|
132
|
-
fail_state=STATE_FAILED,
|
|
133
|
-
max_polls=100,
|
|
134
|
-
)
|
|
135
|
-
return redeem_res
|
|
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
|
+
|
|
28
|
+
|
|
29
|
+
class ProxyWeb3Service(BaseWeb3Service):
|
|
30
|
+
def build_proxy_transaction_request(self, args: dict) -> dict:
|
|
31
|
+
proxy_contract_config = self.get_contract_config()["ProxyContracts"]
|
|
32
|
+
to = proxy_contract_config["ProxyFactory"]
|
|
33
|
+
proxy = derive_proxy_wallet(args["from"], to, PROXY_INIT_CODE_HASH)
|
|
34
|
+
relayer_fee = "0"
|
|
35
|
+
relay_hub = proxy_contract_config["RelayHub"]
|
|
36
|
+
gas_limit_str = self.estimate_gas(
|
|
37
|
+
tx={"from": args["from"], "to": to, "data": args["data"]}
|
|
38
|
+
)
|
|
39
|
+
sig_params = {
|
|
40
|
+
"gasPrice": args.get("gasPrice"),
|
|
41
|
+
"gasLimit": gas_limit_str,
|
|
42
|
+
"relayerFee": relayer_fee,
|
|
43
|
+
"relayHub": relay_hub,
|
|
44
|
+
"relay": args.get("relay"),
|
|
45
|
+
}
|
|
46
|
+
tx_hash = create_struct_hash(
|
|
47
|
+
args["from"],
|
|
48
|
+
to,
|
|
49
|
+
args["data"],
|
|
50
|
+
relayer_fee,
|
|
51
|
+
args.get("gasPrice"),
|
|
52
|
+
gas_limit_str,
|
|
53
|
+
args["nonce"],
|
|
54
|
+
relay_hub,
|
|
55
|
+
args.get("relay"),
|
|
56
|
+
)
|
|
57
|
+
message = {"raw": list(to_bytes(hexstr=tx_hash))}
|
|
58
|
+
|
|
59
|
+
r, s, recovery = secp256k1.sign(
|
|
60
|
+
hash_message(message)[2:], self.clob_client.signer.private_key
|
|
61
|
+
)
|
|
62
|
+
signature = {
|
|
63
|
+
"r": secp256k1.int_to_hex(r, 32),
|
|
64
|
+
"s": secp256k1.int_to_hex(s, 32),
|
|
65
|
+
"v": 28 if recovery else 27,
|
|
66
|
+
"yParity": recovery,
|
|
67
|
+
}
|
|
68
|
+
final_sig = secp256k1.serialize_signature(**signature, to="hex")
|
|
69
|
+
req = {
|
|
70
|
+
"from": args["from"],
|
|
71
|
+
"to": to,
|
|
72
|
+
"proxyWallet": proxy,
|
|
73
|
+
"data": args["data"],
|
|
74
|
+
"nonce": args["nonce"],
|
|
75
|
+
"signature": final_sig,
|
|
76
|
+
"signatureParams": sig_params,
|
|
77
|
+
"type": self.wallet_type.value,
|
|
78
|
+
"metadata": "redeem",
|
|
79
|
+
}
|
|
80
|
+
return req
|
|
81
|
+
|
|
82
|
+
def encode_proxy_transaction_data(self, txns):
|
|
83
|
+
# Prepare the arguments for the 'proxy' function
|
|
84
|
+
calls_data = [
|
|
85
|
+
(txn["typeCode"], txn["to"], txn["value"], txn["data"]) for txn in txns
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Create the contract object
|
|
89
|
+
contract = self.w3.eth.contract(abi=proxy_wallet_factory_abi)
|
|
90
|
+
|
|
91
|
+
# Encode function data
|
|
92
|
+
function_data = contract.encodeABI(fn_name="proxy", args=[calls_data])
|
|
93
|
+
|
|
94
|
+
return function_data
|
|
95
|
+
|
|
96
|
+
def redeem(
|
|
97
|
+
self,
|
|
98
|
+
condition_id: str,
|
|
99
|
+
neg_risk: bool = False,
|
|
100
|
+
redeem_amounts: list[int] | None = None,
|
|
101
|
+
):
|
|
102
|
+
if neg_risk:
|
|
103
|
+
if redeem_amounts is None or len(redeem_amounts) != 2:
|
|
104
|
+
raise Exception("negRisk redeem requires redeem_amounts with length 2")
|
|
105
|
+
tx_data = self.build_neg_risk_redeem_tx_data(condition_id, redeem_amounts)
|
|
106
|
+
tx_to = NEG_RISK_ADAPTER_ADDRESS
|
|
107
|
+
else:
|
|
108
|
+
tx_data = self.build_ctf_redeem_tx_data(condition_id)
|
|
109
|
+
tx_to = CTF_ADDRESS
|
|
110
|
+
tx = {"to": tx_to, "data": tx_data, "value": 0, "typeCode": 1}
|
|
111
|
+
if self.clob_client is None:
|
|
112
|
+
raise Exception("signer not found")
|
|
113
|
+
_from = Web3.to_checksum_address(self.clob_client.get_address())
|
|
114
|
+
rp = self._get_relay_payload(_from, self.wallet_type)
|
|
115
|
+
args = {
|
|
116
|
+
"from": _from,
|
|
117
|
+
"gasPrice": "0",
|
|
118
|
+
"data": self.encode_proxy_transaction_data([tx]),
|
|
119
|
+
"relay": rp["address"],
|
|
120
|
+
"nonce": rp["nonce"],
|
|
121
|
+
}
|
|
122
|
+
req = self.build_proxy_transaction_request(args)
|
|
123
|
+
headers = self.relayer_client._generate_builder_headers(
|
|
124
|
+
"POST", SUBMIT_TRANSACTION, req
|
|
125
|
+
)
|
|
126
|
+
response = requests.post(
|
|
127
|
+
RELAYER_URL + SUBMIT_TRANSACTION, json=req, headers=headers
|
|
128
|
+
).json()
|
|
129
|
+
redeem_res = self.relayer_client.poll_until_state(
|
|
130
|
+
transaction_id=response["transactionID"],
|
|
131
|
+
states=[STATE_MINED, STATE_CONFIRMED],
|
|
132
|
+
fail_state=STATE_FAILED,
|
|
133
|
+
max_polls=100,
|
|
134
|
+
)
|
|
135
|
+
return redeem_res
|
|
136
|
+
|
|
137
|
+
def redeem_all(self) -> list[dict]:
|
|
138
|
+
positions = self.fetch_positions(user_address=self._resolve_user_address())
|
|
139
|
+
if not positions:
|
|
140
|
+
return []
|
|
141
|
+
redeem_list = []
|
|
142
|
+
for pos in positions:
|
|
143
|
+
condition_id = pos.get("conditionId")
|
|
144
|
+
try:
|
|
145
|
+
can_redeem = self.get_redeemable_index_and_balance(condition_id)
|
|
146
|
+
if not can_redeem:
|
|
147
|
+
continue
|
|
148
|
+
if pos.get("negativeRisk"):
|
|
149
|
+
amounts = [0, 0]
|
|
150
|
+
amounts[pos.get("outcomeIndex")] = pos.get("size")
|
|
151
|
+
int_amounts = [int(amount * 1e6) for amount in amounts]
|
|
152
|
+
redeem_res = self.redeem(condition_id=condition_id, redeem_amounts=int_amounts, neg_risk=True)
|
|
153
|
+
else:
|
|
154
|
+
redeem_res = self.redeem(condition_id=condition_id)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"redeem error, {condition_id=}, error={e}")
|
|
157
|
+
else:
|
|
158
|
+
redeem_list.append(redeem_res)
|
|
159
|
+
buy_price = pos.get("avgPrice")
|
|
160
|
+
size = pos.get("size")
|
|
161
|
+
volume = 1 / buy_price * (buy_price * size)
|
|
162
|
+
print(f"{pos.get('slug')} redeem success, volume={volume:.4f} usdc")
|
|
163
|
+
return redeem_list
|