poly-web3 1.0.0__py3-none-any.whl → 1.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.
@@ -24,7 +24,7 @@ if __name__ == "__main__":
24
24
  host,
25
25
  key=os.getenv("POLY_API_KEY"),
26
26
  chain_id=chain_id,
27
- signature_type=1,
27
+ signature_type=1, # signature_type=2 for Safe
28
28
  funder=os.getenv("POLYMARKET_PROXY_ADDRESS"),
29
29
  )
30
30
  creds = client.create_or_derive_api_creds()
@@ -41,27 +41,27 @@ if __name__ == "__main__":
41
41
  )
42
42
  ),
43
43
  )
44
- condition_id = "0x31fb435a9506d14f00b9de5e5e4491cf2223b6d40a2525d9afa8b620b61b50e2"
45
44
  service = PolyWeb3Service(
46
45
  clob_client=client,
47
46
  relayer_client=relayer_client,
48
47
  rpc_url="https://polygon-bor.publicnode.com",
49
48
  )
49
+
50
+ # Redeem all positions that are currently redeemable (returns list or None)
51
+ redeem_all = service.redeem_all(batch_size=10)
52
+ print(redeem_all)
53
+
50
54
  # Redeem in batch
51
55
  condition_ids = [
52
- condition_id,
53
56
  "0x31fb435a9506d14f00b9de5e5e4491cf2223b6d40a2525d9afa8b620b61b50e2",
54
57
  ]
55
58
  redeem_batch = service.redeem(condition_ids, batch_size=10)
56
59
  print(redeem_batch)
57
60
 
58
- # Redeem all positions that are currently redeemable (returns list or None)
59
- redeem_all = service.redeem_all(batch_size=10)
60
- print(redeem_all)
61
61
 
62
62
  # Optional - Query operations (可选操作,用于查询)
63
63
  # can_redeem = service.is_condition_resolved(condition_id)
64
64
  # redeem_balance = service.get_redeemable_index_and_balance(
65
- # condition_id, owner=client.builder.funder
65
+ # condition_id
66
66
  # )
67
67
  # print(can_redeem, redeem_balance)
@@ -0,0 +1,56 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025-12-30 12:00:00
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: example_split_merge.py
6
+ # @Software: PyCharm
7
+ import os
8
+
9
+ import dotenv
10
+ from py_builder_relayer_client.client import RelayClient
11
+ from py_builder_signing_sdk.config import BuilderConfig
12
+ from py_builder_signing_sdk.sdk_types import BuilderApiKeyCreds
13
+ from py_clob_client.client import ClobClient
14
+
15
+ from poly_web3 import RELAYER_URL, PolyWeb3Service
16
+
17
+ dotenv.load_dotenv()
18
+
19
+ if __name__ == "__main__":
20
+ host: str = "https://clob.polymarket.com"
21
+ chain_id: int = 137 # No need to adjust this
22
+ client = ClobClient(
23
+ host,
24
+ key=os.getenv("POLY_API_KEY"),
25
+ chain_id=chain_id,
26
+ signature_type=1, # signature_type=2 for Safe
27
+ funder=os.getenv("POLYMARKET_PROXY_ADDRESS"),
28
+ )
29
+ creds = client.create_or_derive_api_creds()
30
+ client.set_api_creds(creds)
31
+ relayer_client = RelayClient(
32
+ RELAYER_URL,
33
+ chain_id,
34
+ os.getenv("POLY_API_KEY"),
35
+ BuilderConfig(
36
+ local_builder_creds=BuilderApiKeyCreds(
37
+ key=os.getenv("BUILDER_KEY"),
38
+ secret=os.getenv("BUILDER_SECRET"),
39
+ passphrase=os.getenv("BUILDER_PASSPHRASE"),
40
+ )
41
+ ),
42
+ )
43
+ service = PolyWeb3Service(
44
+ clob_client=client,
45
+ relayer_client=relayer_client,
46
+ rpc_url="https://polygon-bor.publicnode.com",
47
+ )
48
+
49
+ condition_id = "0x58ec217262554683a6c1fa2de9d87addef26dd3348367824d6890c68a98809b0"
50
+ amount = 10 # amount in human USDC units
51
+
52
+ split_result = service.split(condition_id, amount)
53
+ print(split_result)
54
+
55
+ merge_result = service.merge(condition_id, amount)
56
+ print(merge_result)
poly_web3/const.py CHANGED
@@ -4,7 +4,7 @@
4
4
  # @Site:
5
5
  # @File: const.py
6
6
  # @Software: PyCharm
7
- from web3 import Web3
7
+ from eth_utils import to_checksum_address
8
8
 
9
9
  GET_NONCE = "/nonce"
10
10
  GET_RELAY_PAYLOAD = "/relay-payload"
@@ -15,7 +15,7 @@ GET_DEPLOYED = "/deployed"
15
15
  RPC_URL = "https://polygon-bor.publicnode.com" # "https://polygon-rpc.com"
16
16
  RELAYER_URL = "https://relayer-v2.polymarket.com"
17
17
 
18
- STATE_NEW = ("STATE_NEW",)
18
+ STATE_NEW = "STATE_NEW"
19
19
  STATE_EXECUTED = "STATE_EXECUTED"
20
20
  STATE_MINED = "STATE_MINED"
21
21
  STATE_INVALID = "STATE_INVALID"
@@ -23,13 +23,13 @@ STATE_CONFIRMED = "STATE_CONFIRMED"
23
23
  STATE_FAILED = "STATE_FAILED"
24
24
 
25
25
  # address
26
- CTF_ADDRESS = Web3.to_checksum_address("0x4d97dcd97ec945f40cf65f87097ace5ea0476045")
27
- USDC_POLYGON = Web3.to_checksum_address("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")
28
- NEG_RISK_ADAPTER_ADDRESS = Web3.to_checksum_address(
26
+ CTF_ADDRESS = to_checksum_address("0x4d97dcd97ec945f40cf65f87097ace5ea0476045")
27
+ USDC_POLYGON = to_checksum_address("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")
28
+ NEG_RISK_ADAPTER_ADDRESS = to_checksum_address(
29
29
  "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
30
30
  )
31
31
  ZERO_BYTES32 = "0x" + "00" * 32
32
- proxy_factory_address = Web3.to_checksum_address(
32
+ proxy_factory_address = to_checksum_address(
33
33
  "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
34
34
  )
35
35
  SAFE_INIT_CODE_HASH = (
@@ -78,6 +78,70 @@ CTF_ABI_REDEEM = [
78
78
  }
79
79
  ]
80
80
 
81
+ CTF_ABI_SPLIT = [
82
+ {
83
+ "name": "splitPosition",
84
+ "type": "function",
85
+ "stateMutability": "nonpayable",
86
+ "inputs": [
87
+ {"name": "collateralToken", "type": "address"},
88
+ {"name": "parentCollectionId", "type": "bytes32"},
89
+ {"name": "conditionId", "type": "bytes32"},
90
+ {"name": "partition", "type": "uint256[]"},
91
+ {"name": "amount", "type": "uint256"},
92
+ ],
93
+ "outputs": [],
94
+ }
95
+ ]
96
+
97
+ CTF_ABI_MERGE = [
98
+ {
99
+ "name": "mergePositions",
100
+ "type": "function",
101
+ "stateMutability": "nonpayable",
102
+ "inputs": [
103
+ {"name": "collateralToken", "type": "address"},
104
+ {"name": "parentCollectionId", "type": "bytes32"},
105
+ {"name": "conditionId", "type": "bytes32"},
106
+ {"name": "partition", "type": "uint256[]"},
107
+ {"name": "amount", "type": "uint256"},
108
+ ],
109
+ "outputs": [],
110
+ }
111
+ ]
112
+
113
+ CTF_ABI_SPLIT = [
114
+ {
115
+ "name": "splitPosition",
116
+ "type": "function",
117
+ "stateMutability": "nonpayable",
118
+ "inputs": [
119
+ {"name": "collateralToken", "type": "address"},
120
+ {"name": "parentCollectionId", "type": "bytes32"},
121
+ {"name": "conditionId", "type": "bytes32"},
122
+ {"name": "partition", "type": "uint256[]"},
123
+ {"name": "amount", "type": "uint256"},
124
+ ],
125
+ "outputs": [],
126
+ }
127
+ ]
128
+
129
+ CTF_ABI_MERGE = [
130
+ {
131
+ "name": "mergePositions",
132
+ "type": "function",
133
+ "stateMutability": "nonpayable",
134
+ "inputs": [
135
+ {"name": "collateralToken", "type": "address"},
136
+ {"name": "parentCollectionId", "type": "bytes32"},
137
+ {"name": "conditionId", "type": "bytes32"},
138
+ {"name": "partition", "type": "uint256[]"},
139
+ {"name": "amount", "type": "uint256"},
140
+ ],
141
+ "outputs": [],
142
+ }
143
+ ]
144
+
81
145
  NEG_RISK_ADAPTER_ABI_REDEEM = [
82
146
  {
83
147
  "name": "redeemPositions",
@@ -5,9 +5,11 @@
5
5
  # @File: base.py
6
6
  # @Software: PyCharm
7
7
  from typing import Any
8
+ from decimal import Decimal, InvalidOperation, ROUND_DOWN
8
9
 
9
10
  from py_builder_relayer_client.client import RelayClient
10
11
  from py_clob_client.client import ClobClient
12
+ from eth_utils import to_checksum_address
11
13
  from web3 import Web3
12
14
  import requests
13
15
 
@@ -18,6 +20,8 @@ from poly_web3.const import (
18
20
  ZERO_BYTES32,
19
21
  USDC_POLYGON,
20
22
  CTF_ABI_REDEEM,
23
+ CTF_ABI_SPLIT,
24
+ CTF_ABI_MERGE,
21
25
  NEG_RISK_ADAPTER_ADDRESS,
22
26
  RELAYER_URL,
23
27
  POL,
@@ -132,7 +136,7 @@ class BaseWeb3Service:
132
136
  if not winners:
133
137
  return []
134
138
  ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
135
- owner_checksum = Web3.to_checksum_address(owner)
139
+ owner_checksum = to_checksum_address(owner)
136
140
  redeemable: list[tuple] = []
137
141
  for index in winners:
138
142
  index_set = 1 << index
@@ -149,7 +153,6 @@ class BaseWeb3Service:
149
153
 
150
154
  def build_ctf_redeem_tx_data(self, condition_id: str) -> str:
151
155
  ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_REDEEM)
152
- # 只需要 calldata:encodeABI 即可
153
156
  return ctf.functions.redeemPositions(
154
157
  USDC_POLYGON,
155
158
  ZERO_BYTES32,
@@ -157,6 +160,40 @@ class BaseWeb3Service:
157
160
  [1, 2],
158
161
  )._encode_transaction_data()
159
162
 
163
+ def build_ctf_split_tx_data(
164
+ self,
165
+ condition_id: str,
166
+ partition: list[int],
167
+ amount: int,
168
+ collateral_token: str = USDC_POLYGON,
169
+ parent_collection_id: str = ZERO_BYTES32,
170
+ ) -> str:
171
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_SPLIT)
172
+ return ctf.functions.splitPosition(
173
+ collateral_token,
174
+ parent_collection_id,
175
+ condition_id,
176
+ partition,
177
+ amount,
178
+ )._encode_transaction_data()
179
+
180
+ def build_ctf_merge_tx_data(
181
+ self,
182
+ condition_id: str,
183
+ partition: list[int],
184
+ amount: int,
185
+ collateral_token: str = USDC_POLYGON,
186
+ parent_collection_id: str = ZERO_BYTES32,
187
+ ) -> str:
188
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_MERGE)
189
+ return ctf.functions.mergePositions(
190
+ collateral_token,
191
+ parent_collection_id,
192
+ condition_id,
193
+ partition,
194
+ amount,
195
+ )._encode_transaction_data()
196
+
160
197
  def build_neg_risk_redeem_tx_data(
161
198
  self, condition_id: str, redeem_amounts: list[int]
162
199
  ) -> str:
@@ -171,6 +208,9 @@ class BaseWeb3Service:
171
208
  def _build_redeem_tx(self, to: str, data: str) -> Any:
172
209
  raise NotImplementedError("redeem tx builder not implemented")
173
210
 
211
+ def _build_ctf_tx(self, to: str, data: str) -> Any:
212
+ return self._build_redeem_tx(to, data)
213
+
174
214
  def _build_redeem_txs_from_positions(self, positions: list[dict]) -> list[Any]:
175
215
  neg_amounts_by_condition: dict[str, list[float]] = {}
176
216
  normal_conditions: set[str] = set()
@@ -207,8 +247,11 @@ class BaseWeb3Service:
207
247
  )
208
248
  return txs
209
249
 
250
+ def _submit_transactions(self, txs: list[Any], metadata: str) -> dict | None:
251
+ raise NotImplementedError("transaction submit not implemented")
252
+
210
253
  def _submit_redeem(self, txs: list[Any]) -> dict | None:
211
- raise NotImplementedError("redeem submit not implemented")
254
+ return self._submit_transactions(txs, "redeem")
212
255
 
213
256
  def _redeem_batch(self, condition_ids: list[str], batch_size: int) -> list[dict]:
214
257
  """
@@ -264,7 +307,7 @@ class BaseWeb3Service:
264
307
  )
265
308
  except Exception as e:
266
309
  error_list.extend(batch)
267
- logger.info(f"redeem batch error, {batch=}, error={e}")
310
+ logger.error(f"redeem batch error, {batch=}, error={e}")
268
311
  if error_list:
269
312
  logger.warning(f"error redeem condition list, {error_list}")
270
313
  return redeem_list
@@ -312,6 +355,26 @@ class BaseWeb3Service:
312
355
  for i in range(0, len(condition_ids), batch_size)
313
356
  ]
314
357
 
358
+ @staticmethod
359
+ def _to_usdc_base_units(amount: int | float | str | Decimal) -> int:
360
+ try:
361
+ if isinstance(amount, Decimal):
362
+ human = amount
363
+ elif isinstance(amount, int):
364
+ human = Decimal(amount)
365
+ else:
366
+ human = Decimal(str(amount))
367
+ except (InvalidOperation, ValueError) as exc:
368
+ raise Exception(f"invalid amount: {amount}") from exc
369
+ if human <= 0:
370
+ raise Exception("amount must be greater than 0")
371
+ base_units = (human * Decimal("1000000")).quantize(
372
+ Decimal("1"), rounding=ROUND_DOWN
373
+ )
374
+ if base_units <= 0:
375
+ raise Exception("amount too small after conversion")
376
+ return int(base_units)
377
+
315
378
  def redeem(
316
379
  self,
317
380
  condition_ids: str | list[str],
@@ -330,3 +393,49 @@ class BaseWeb3Service:
330
393
  """
331
394
  positions = self.fetch_positions(user_address=self._resolve_user_address())
332
395
  return self._redeem_from_positions(positions, batch_size)
396
+
397
+ def split(
398
+ self,
399
+ condition_id: str,
400
+ amount: int | float | str | Decimal,
401
+ collateral_token: str = USDC_POLYGON,
402
+ parent_collection_id: str = ZERO_BYTES32,
403
+ ) -> dict | None:
404
+ """
405
+ Split a position for binary markets (Yes/No), amount in human units.
406
+ """
407
+ amount_base_units = self._to_usdc_base_units(amount)
408
+ tx = self._build_ctf_tx(
409
+ CTF_ADDRESS,
410
+ self.build_ctf_split_tx_data(
411
+ condition_id=condition_id,
412
+ partition=[1, 2],
413
+ amount=amount_base_units,
414
+ collateral_token=collateral_token,
415
+ parent_collection_id=parent_collection_id,
416
+ ),
417
+ )
418
+ return self._submit_transactions([tx], "split")
419
+
420
+ def merge(
421
+ self,
422
+ condition_id: str,
423
+ amount: int | float | str | Decimal,
424
+ collateral_token: str = USDC_POLYGON,
425
+ parent_collection_id: str = ZERO_BYTES32,
426
+ ) -> dict | None:
427
+ """
428
+ Merge binary positions (Yes/No) back into a single position, amount in human units.
429
+ """
430
+ amount_base_units = self._to_usdc_base_units(amount)
431
+ tx = self._build_ctf_tx(
432
+ CTF_ADDRESS,
433
+ self.build_ctf_merge_tx_data(
434
+ condition_id=condition_id,
435
+ partition=[1, 2],
436
+ amount=amount_base_units,
437
+ collateral_token=collateral_token,
438
+ parent_collection_id=parent_collection_id,
439
+ ),
440
+ )
441
+ return self._submit_transactions([tx], "merge")
@@ -4,6 +4,9 @@
4
4
  # @Site:
5
5
  # @File: eoa_service.py
6
6
  # @Software: PyCharm
7
+ from decimal import Decimal
8
+
9
+ from poly_web3.const import USDC_POLYGON, ZERO_BYTES32
7
10
  from poly_web3.web3_service.base import BaseWeb3Service
8
11
 
9
12
 
@@ -17,3 +20,21 @@ class EOAWeb3Service(BaseWeb3Service):
17
20
 
18
21
  def redeem_all(self, batch_size: int = 10) -> list[dict]:
19
22
  raise ImportError("EOA wallet redeem not supported")
23
+
24
+ def split(
25
+ self,
26
+ condition_id: str,
27
+ amount: int | float | str | Decimal,
28
+ collateral_token: str = USDC_POLYGON,
29
+ parent_collection_id: str = ZERO_BYTES32,
30
+ ) -> dict | None:
31
+ raise ImportError("EOA wallet split not supported")
32
+
33
+ def merge(
34
+ self,
35
+ condition_id: str,
36
+ amount: int | float | str | Decimal,
37
+ collateral_token: str = USDC_POLYGON,
38
+ parent_collection_id: str = ZERO_BYTES32,
39
+ ) -> dict | None:
40
+ raise ImportError("EOA wallet merge not supported")
@@ -6,8 +6,7 @@
6
6
  # @Software: PyCharm
7
7
  import requests
8
8
 
9
- from web3 import Web3
10
- from eth_utils import to_bytes
9
+ from eth_utils import to_bytes, to_checksum_address
11
10
 
12
11
  from poly_web3.const import (
13
12
  proxy_wallet_factory_abi,
@@ -22,6 +21,8 @@ from poly_web3.web3_service.base import BaseWeb3Service
22
21
  from poly_web3.signature.build import derive_proxy_wallet, create_struct_hash
23
22
  from poly_web3.signature.hash_message import hash_message
24
23
  from poly_web3.signature import secp256k1
24
+
25
+
25
26
  class ProxyWeb3Service(BaseWeb3Service):
26
27
  def _build_redeem_tx(self, to: str, data: str) -> dict:
27
28
  return {
@@ -31,7 +32,9 @@ class ProxyWeb3Service(BaseWeb3Service):
31
32
  "typeCode": 1,
32
33
  }
33
34
 
34
- def build_proxy_transaction_request(self, args: dict) -> dict:
35
+ def build_proxy_transaction_request(
36
+ self, args: dict, metadata: str = "redeem"
37
+ ) -> dict:
35
38
  proxy_contract_config = self.get_contract_config()["ProxyContracts"]
36
39
  to = proxy_contract_config["ProxyFactory"]
37
40
  proxy = derive_proxy_wallet(args["from"], to, PROXY_INIT_CODE_HASH)
@@ -79,7 +82,7 @@ class ProxyWeb3Service(BaseWeb3Service):
79
82
  "signature": final_sig,
80
83
  "signatureParams": sig_params,
81
84
  "type": self.wallet_type.value,
82
- "metadata": "redeem",
85
+ "metadata": metadata,
83
86
  }
84
87
  return req
85
88
 
@@ -92,15 +95,13 @@ class ProxyWeb3Service(BaseWeb3Service):
92
95
  # Create the contract object
93
96
  contract = self.w3.eth.contract(abi=proxy_wallet_factory_abi)
94
97
 
95
- # Encode function data
96
- function_data = contract.encodeABI(fn_name="proxy", args=[calls_data])
97
-
98
- return function_data
98
+ # Encode function data (compatible with web3 6/7)
99
+ return contract.functions.proxy(calls_data)._encode_transaction_data()
99
100
 
100
- def _submit_redeem(self, txs: list[dict]) -> dict:
101
+ def _submit_transactions(self, txs: list[dict], metadata: str) -> dict:
101
102
  if self.clob_client is None:
102
103
  raise Exception("signer not found")
103
- _from = Web3.to_checksum_address(self.clob_client.get_address())
104
+ _from = to_checksum_address(self.clob_client.get_address())
104
105
  rp = self._get_relay_payload(_from, self.wallet_type)
105
106
  args = {
106
107
  "from": _from,
@@ -109,7 +110,7 @@ class ProxyWeb3Service(BaseWeb3Service):
109
110
  "relay": rp["address"],
110
111
  "nonce": rp["nonce"],
111
112
  }
112
- req = self.build_proxy_transaction_request(args)
113
+ req = self.build_proxy_transaction_request(args, metadata=metadata)
113
114
  headers = self.relayer_client._generate_builder_headers(
114
115
  "POST", SUBMIT_TRANSACTION, req
115
116
  )
@@ -123,3 +124,6 @@ class ProxyWeb3Service(BaseWeb3Service):
123
124
  max_polls=100,
124
125
  )
125
126
  return redeem_res
127
+
128
+ def _submit_redeem(self, txs: list[dict]) -> dict:
129
+ return self._submit_transactions(txs, "redeem")
@@ -17,8 +17,13 @@ class SafeWeb3Service(BaseWeb3Service):
17
17
  operation=OperationType.Call,
18
18
  )
19
19
 
20
- def _submit_redeem(self, txs: list[SafeTransaction]) -> dict | None:
20
+ def _submit_transactions(
21
+ self, txs: list[SafeTransaction], metadata: str
22
+ ) -> dict | None:
21
23
  if self.relayer_client is None:
22
24
  raise Exception("relayer_client not found")
23
- resp = self.relayer_client.execute(txs, "redeem")
25
+ resp = self.relayer_client.execute(txs, metadata)
24
26
  return resp.wait()
27
+
28
+ def _submit_redeem(self, txs: list[SafeTransaction]) -> dict | None:
29
+ return self._submit_transactions(txs, "redeem")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poly-web3
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Polymarket Proxy wallet redeem SDK - Execute redeem operations on Polymarket using proxy wallets
5
5
  Home-page: https://github.com/tosmart01/poly-web3
6
6
  Author: PinBar
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: py-clob-client>=0.25.0
21
21
  Requires-Dist: py-builder-relayer-client>=0.0.1
22
- Requires-Dist: web3==6.8
22
+ Requires-Dist: web3<8,>=7.0.0
23
23
  Requires-Dist: eth-utils==5.3.1
24
24
  Requires-Dist: setuptools>=80.9.0
25
25
  Dynamic: home-page
@@ -27,16 +27,61 @@ Dynamic: requires-python
27
27
 
28
28
  # poly-web3
29
29
 
30
- Python SDK for Polymarket Proxy wallet redeem operations. Supports executing Conditional Token Fund (CTF) redeem operations on Polymarket through proxy wallets, Free gas.
30
+ ![PyPI](https://img.shields.io/pypi/v/poly-web3)
31
+ ![Python](https://img.shields.io/pypi/pyversions/poly-web3)
32
+ ![License](https://img.shields.io/github/license/tosmart01/poly-web3)
33
+
34
+ Python SDK for redeeming and splitting/merging Polymarket positions via Proxy/Safe wallets (gas-free).
31
35
 
32
36
  [English](README.md) | [中文](README.zh.md)
33
37
 
38
+ ```bash
39
+ Python >= 3.11
40
+ pip install poly-web3
41
+ ```
42
+
43
+ ```python
44
+ from poly_web3 import PolyWeb3Service
45
+
46
+ service = PolyWeb3Service(
47
+ clob_client=client,
48
+ relayer_client=relayer_client,
49
+ )
50
+
51
+ # Redeem all redeemable positions for the current account.
52
+ service.redeem_all(batch_size=10)
53
+
54
+ # Split/Merge for binary markets (amount in human USDC units).
55
+ service.split("0x...", 10)
56
+ service.merge("0x...", 10)
57
+ ```
58
+
59
+ [See the full example](#quick-start)
60
+
61
+ ## Redeem Behavior Notes
62
+
63
+ - Redeemable positions are fetched via the official Positions API, which typically has ~1 minute latency.
64
+ - `redeem_all` returns an empty list if there are no redeemable positions. If the returned list contains `None`, the redeem failed and should be retried.
65
+
66
+ ## Split/Merge Notes
67
+
68
+ - `split`/`merge` are designed for binary markets (Yes/No) and use the default partition internally.
69
+ - `amount` is in human units (USDC), and is converted to base units internally.
70
+
71
+ ## FAQ
72
+
73
+ 1. **UI shows redeemable, but `redeem_all` returns `[]`**: The official Positions API can be delayed by 1–3 minutes. Wait a bit and retry.
74
+ 2. **RPC error during redeem**: Switch RPC endpoints by setting `rpc_url` when instantiating `PolyWeb3Service`.
75
+ 3. **Redeem stuck in `execute`**: The official relayer may be congested. Stop redeeming for 1 hour to avoid nonce looping from repeated submissions.
76
+ 4. **Relayer client returns 403**: You need to apply for Builder API access and use a valid key. Reference: Polymarket Builders — Introduction: https://docs.polymarket.com/developers/builders/builder-intro
77
+ 5. **Relayer daily limit**: The official relayer typically limits to 100 requests per day. Prefer batch redeem (`batch_size`) to reduce the number of requests and avoid hitting the limit.
78
+
34
79
  ## About the Project
35
80
 
36
- This project is a Python rewrite of Polymarket's official TypeScript implementation of `builder-relayer-client`, designed to provide Python developers with a convenient tool for executing proxy wallet redeem operations on Polymarket.
81
+ This project is a Python rewrite of Polymarket's official TypeScript implementation of `builder-relayer-client`, designed to provide Python developers with a convenient tool for executing Proxy and Safe wallet redeem operations on Polymarket.
37
82
 
38
83
  **Important Notes:**
39
- - This project **only implements the official redeem functionality**, focusing on Conditional Token Fund (CTF) redeem operations
84
+ - This project implements official CTF redeem plus binary split/merge operations
40
85
  - Other features (such as trading, order placement, etc.) are not within the scope of this project
41
86
 
42
87
  **Some Polymarket-related redeem or write operations implemented in this project depend on access granted through Polymarket's Builder program. To perform real redeem operations against Polymarket, you must apply for and obtain a Builder key/credentials via Polymarket's official Builder application process. After approval you will receive the credentials required to use the Builder API—only then will the redeem flows in this repository work against the live service. For local development or automated tests, use mocks or testnet setups instead of real keys to avoid exposing production credentials.**
@@ -45,19 +90,11 @@ Reference:
45
90
  - Polymarket Builders — Introduction: https://docs.polymarket.com/developers/builders/builder-intro
46
91
 
47
92
  **Current Status:**
48
- - ✅ **Proxy Wallet** - Fully supported for redeem functionality
49
- - 🚧 **Safe Wallet** - Under development
93
+ - ✅ **Proxy Wallet** - Fully supported for redeem/split/merge
94
+ - **Safe Wallet** - Fully supported for redeem/split/merge
50
95
  - 🚧 **EOA Wallet** - Under development
51
96
 
52
- We welcome community contributions! If you'd like to help implement Safe or EOA wallet redeem functionality, or have other improvement suggestions, please feel free to submit a Pull Request.
53
-
54
- ## Features
55
-
56
- - ✅ Support for Polymarket Proxy wallet redeem operations (currently only Proxy wallet is supported)
57
- - ✅ Check if conditions are resolved
58
- - ✅ Get redeemable indexes and balances
59
- - ✅ Support for standard CTF redeem and negative risk (neg_risk) redeem
60
- - ✅ Automatic transaction execution through Relayer service
97
+ We welcome community contributions! If you'd like to help implement EOA wallet redeem functionality, or have other improvement suggestions, please feel free to submit a Pull Request.
61
98
 
62
99
  ## Installation
63
100
 
@@ -79,7 +116,7 @@ uv add poly-web3
79
116
 
80
117
  - `py-clob-client >= 0.25.0` - Polymarket CLOB client
81
118
  - `py-builder-relayer-client >= 0.0.1` - Builder Relayer client
82
- - `web3 == 6.8` - Web3.py library
119
+ - `web3 >= 7.0.0` - Web3.py library
83
120
  - `eth-utils == 5.3.1` - Ethereum utilities library
84
121
 
85
122
  ## Quick Start
@@ -104,7 +141,7 @@ client = ClobClient(
104
141
  host,
105
142
  key=os.getenv("POLY_API_KEY"),
106
143
  chain_id=chain_id,
107
- signature_type=1, # Proxy wallet type
144
+ signature_type=1, # Proxy wallet type (signature_type=2 for Safe)
108
145
  funder=os.getenv("POLYMARKET_PROXY_ADDRESS"),
109
146
  )
110
147
 
@@ -132,35 +169,39 @@ service = PolyWeb3Service(
132
169
  )
133
170
 
134
171
 
172
+ # Redeem all positions that are currently redeemable
173
+ redeem_all_result = service.redeem_all(batch_size=10)
174
+ print(f"Redeem all result: {redeem_all_result}")
175
+ # If redeem_all_result contains None, refer to README FAQ and retry.
176
+ if redeem_all_result and any(item is None for item in redeem_all_result):
177
+ print("Redeem failed for some items; please retry.")
178
+
135
179
  # Execute redeem operation (batch)
136
180
  condition_ids = [
137
181
  "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c",
138
182
  "0x31fb435a9506d14f00b9de5e5e4491cf2223b6d40a2525d9afa8b620b61b50e2",
139
183
  ]
140
- redeem_batch_result = service.redeem(condition_ids, batch_size=20)
184
+ redeem_batch_result = service.redeem(condition_ids, batch_size=10)
141
185
  print(f"Redeem batch result: {redeem_batch_result}")
142
-
143
- # Redeem all positions that are currently redeemable
144
- redeem_all_result = service.redeem_all(batch_size=20)
145
- print(f"Redeem all result: {redeem_all_result}")
186
+ if redeem_all_result and any(item is None for item in redeem_all_result):
187
+ print("Redeem failed for some items; please retry.")
146
188
  ```
147
189
 
148
- ### Optional - Query Operations
149
-
150
- Before executing redeem, you can optionally check the condition status and query redeemable balances:
190
+ ### Basic Usage - Split/Merge (Binary Markets)
151
191
 
152
192
  ```python
153
- # Check if condition is resolved
154
- condition_id = "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c"
155
- can_redeem = service.is_condition_resolved(condition_id)
156
-
157
- # Get redeemable indexes and balances
158
- redeem_balance = service.get_redeemable_index_and_balance(
159
- condition_id, owner=client.builder.funder
193
+ # amount is in human units (USDC)
194
+ split_result = service.split(
195
+ "0x31fb435a9506d14f00b9de5e5e4491cf2223b6d40a2525d9afa8b620b61b50e2",
196
+ 1.5,
160
197
  )
198
+ print(f"Split result: {split_result}")
161
199
 
162
- print(f"Can redeem: {can_redeem}")
163
- print(f"Redeemable balance: {redeem_balance}")
200
+ merge_result = service.merge(
201
+ "0x31fb435a9506d14f00b9de5e5e4491cf2223b6d40a2525d9afa8b620b61b50e2",
202
+ 1.5,
203
+ )
204
+ print(f"Merge result: {merge_result}")
164
205
  ```
165
206
 
166
207
  ## API Documentation
@@ -171,6 +212,74 @@ The main service class that automatically selects the appropriate service implem
171
212
 
172
213
  #### Methods
173
214
 
215
+ ##### `redeem(condition_ids: list[str], batch_size: int = 20)`
216
+
217
+ Execute redeem operation.
218
+
219
+ **Parameters:**
220
+ - `condition_ids` (list[str]): List of condition IDs
221
+ - `batch_size` (int): Batch size for redeem requests
222
+
223
+ **Returns:**
224
+ - `dict | list[dict]`: Transaction result(s) containing transaction status and related information
225
+
226
+ **Examples:**
227
+
228
+ ```python
229
+ # Batch redeem
230
+ result = service.redeem(["0x...", "0x..."], batch_size=10)
231
+ ```
232
+
233
+ ##### `redeem_all(batch_size: int = 20) -> list[dict]`
234
+
235
+ Redeem all positions that are currently redeemable for the authenticated account.
236
+
237
+ **Returns:**
238
+ - `list[dict]`: List of redeem results; empty list if no redeemable positions. If the list contains `None`, the redeem failed and should be retried.
239
+
240
+ **Examples:**
241
+
242
+ ```python
243
+ # Redeem all positions that can be redeemed
244
+ service.redeem_all(batch_size=10)
245
+ ```
246
+
247
+ ##### `split(condition_id: str, amount: int | float | str)`
248
+
249
+ Split a binary (Yes/No) position. `amount` is in human USDC units.
250
+
251
+ **Parameters:**
252
+ - `condition_id` (str): Condition ID
253
+ - `amount` (int | float | str): Amount in USDC
254
+
255
+ **Returns:**
256
+ - `dict | None`: Transaction result
257
+
258
+ **Examples:**
259
+
260
+ ```python
261
+ result = service.split("0x...", 1.25)
262
+ ```
263
+
264
+ ##### `merge(condition_id: str, amount: int | float | str)`
265
+
266
+ Merge a binary (Yes/No) position. `amount` is in human USDC units.
267
+
268
+ **Parameters:**
269
+ - `condition_id` (str): Condition ID
270
+ - `amount` (int | float | str): Amount in USDC
271
+
272
+ **Returns:**
273
+ - `dict | None`: Transaction result
274
+
275
+ **Examples:**
276
+
277
+ ```python
278
+ result = service.merge("0x...", 1.25)
279
+ ```
280
+
281
+ #### Optional APIs
282
+
174
283
  ##### `is_condition_resolved(condition_id: str) -> bool`
175
284
 
176
285
  Check if the specified condition is resolved.
@@ -202,36 +311,22 @@ Get redeemable indexes and balances for the specified address.
202
311
  **Returns:**
203
312
  - `list[tuple]`: List of tuples containing (index, balance), balance is in USDC units
204
313
 
205
- ##### `redeem(condition_ids: list[str], batch_size: int = 20)`
206
-
207
- Execute redeem operation.
208
-
209
- **Parameters:**
210
- - `condition_ids` (list[str]): List of condition IDs
211
- - `batch_size` (int): Batch size for redeem requests
212
-
213
- **Returns:**
214
- - `dict | list[dict]`: Transaction result(s) containing transaction status and related information
314
+ ## Optional: Query Operations
215
315
 
216
- **Examples:**
316
+ Before executing redeem, you can optionally check the condition status and query redeemable balances:
217
317
 
218
318
  ```python
219
- # Batch redeem
220
- result = service.redeem(["0x...", "0x..."], batch_size=20)
221
- ```
222
-
223
- ##### `redeem_all(batch_size: int = 20) -> list[dict] | None`
224
-
225
- Redeem all positions that are currently redeemable for the authenticated account.
226
-
227
- **Returns:**
228
- - `list[dict] | None`: List of redeem results, or `None` if no redeemable positions
319
+ # Check if condition is resolved
320
+ condition_id = "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c"
321
+ can_redeem = service.is_condition_resolved(condition_id)
229
322
 
230
- **Examples:**
323
+ # Get redeemable indexes and balances
324
+ redeem_balance = service.get_redeemable_index_and_balance(
325
+ condition_id, owner=client.builder.funder
326
+ )
231
327
 
232
- ```python
233
- # Redeem all positions that can be redeemed
234
- service.redeem_all(batch_size=20)
328
+ print(f"Can redeem: {can_redeem}")
329
+ print(f"Redeemable balance: {redeem_balance}")
235
330
  ```
236
331
 
237
332
  ## Project Structure
@@ -249,14 +344,14 @@ poly_web3/
249
344
  ├── base.py # Base service class
250
345
  ├── proxy_service.py # Proxy wallet service (✅ Implemented)
251
346
  ├── eoa_service.py # EOA wallet service (🚧 Under development)
252
- └── safe_service.py # Safe wallet service (🚧 Under development)
347
+ └── safe_service.py # Safe wallet service ( Implemented)
253
348
  ```
254
349
 
255
350
  ## Notes
256
351
 
257
352
  1. **Environment Variable Security**: Make sure `.env` file is added to `.gitignore`, do not commit sensitive information to the code repository
258
353
  2. **Network Support**: Currently mainly supports Polygon mainnet (chain_id: 137), Amoy testnet may have limited functionality
259
- 3. **Wallet Type**: **Currently only Proxy wallet is supported** (signature_type: 1), Safe and EOA wallet redeem functionality is under development
354
+ 3. **Wallet Type**: Proxy (signature_type: 1) and Safe (signature_type: 2) are supported; EOA wallet operations are under development
260
355
  4. **Gas Fees**: Transactions are executed through Relayer, gas fees are handled by the Relayer
261
356
 
262
357
  ## Development
@@ -271,18 +366,18 @@ uv pip install -e ".[dev]"
271
366
 
272
367
  ```bash
273
368
  python examples/example_redeem.py
369
+ python examples/example_split_merge.py
274
370
  ```
275
371
 
276
372
  ### Contributing
277
373
 
278
- We welcome all forms of contributions! If you'd like to:
279
-
280
- - Implement Safe or EOA wallet support
281
- - Fix bugs or improve existing functionality
282
- - Add new features or improve documentation
283
- - Make suggestions or report issues
374
+ Simple contribution flow:
284
375
 
285
- Please feel free to submit an Issue or Pull Request. Your contributions will help make this project better!
376
+ 1. Open an Issue to describe the change (bug/feature/doc).
377
+ 2. Fork and create a branch: `feat/xxx` or `fix/xxx`.
378
+ 3. Make changes and update/add docs if needed.
379
+ 4. Run: `uv run python -m examples.example_redeem` or `uv run python -m examples.example_split_merge` (if applicable).
380
+ 5. Open a Pull Request and link the Issue.
286
381
 
287
382
  ## License
288
383
 
@@ -0,0 +1,19 @@
1
+ examples/example_redeem.py,sha256=r4tNcrhOIMBebZ1eVudswQKoMjhNZ-BkujNqx0w0sAI,2041
2
+ examples/example_split_merge.py,sha256=hM76YUBNakfwpWgRQ5rAGlMnVVzrEJvA5rtOJvLzwHs,1704
3
+ poly_web3/__init__.py,sha256=O4xBBGCQoO453RdGZ9eWpjpOEOz_kFoU_TTnigbBaPs,1078
4
+ poly_web3/const.py,sha256=uEH42ac-AHqSQSZGmB57IygJl_1C5hGs4JiDM9y2QZo,6858
5
+ poly_web3/log.py,sha256=NPY6X7Ysw-5ds3JkA26vhfge2Dc39ptezmCWYbwT-FM,986
6
+ poly_web3/schema.py,sha256=GTzpEy8-mqw2fCqoQKQFn6ieyhXdSnW6I9YQMKGnnhM,435
7
+ poly_web3/signature/__init__.py,sha256=p4EwohjUtwEsFqrc7eeqY5sQa14GWc8BdR6km8c7SH8,123
8
+ poly_web3/signature/build.py,sha256=PEyMFbE2PM2Xvz3wvmA6T6FVnU3mB7uanoEutbneFQ8,3614
9
+ poly_web3/signature/hash_message.py,sha256=hREtjuOdLcm_IlBz1YuzyrObVdxs7Yn-ULfh09PeNmk,1319
10
+ poly_web3/signature/secp256k1.py,sha256=IBgp0vNJyskU7ZUcWCdDttrzmJgEWH6lakB74whur_w,1500
11
+ poly_web3/web3_service/__init__.py,sha256=HNRh06yZNYXDmGS69ISXG7_PPG9foGXLyHOdnbgYXk0,315
12
+ poly_web3/web3_service/base.py,sha256=fEWn1CTfYDjQlPYopMGg2UTnZuxfIEVGfedtfDh4Y0Q,16036
13
+ poly_web3/web3_service/eoa_service.py,sha256=H9brj_em-o66O4JlJfQTcQlUvdw08-_6gKReIbxi79c,1159
14
+ poly_web3/web3_service/proxy_service.py,sha256=ztT2WGoj34Q6EN8UISlWatid-oSzY83W0W23V3u6Oos,4370
15
+ poly_web3/web3_service/safe_service.py,sha256=Fjb0x_e9K2gAOhh0sF6JM9zbizx5zLbYbSrZvH1sXpQ,945
16
+ poly_web3-1.0.2.dist-info/METADATA,sha256=5MCq15yl5ifEpr2oXZb-ckHFPJwlJ3So_FJ8aHgdf0M,12834
17
+ poly_web3-1.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
+ poly_web3-1.0.2.dist-info/top_level.txt,sha256=wW40wsocHfhFgqSWWvYWrhwKGQU5IWkhyaz5J90vmHo,19
19
+ poly_web3-1.0.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- examples/example_redeem.py,sha256=grSaAsmjgQDKd1zpd17-cYkAv7fkqFUqDEvCr3gXJtA,2149
2
- poly_web3/__init__.py,sha256=O4xBBGCQoO453RdGZ9eWpjpOEOz_kFoU_TTnigbBaPs,1078
3
- poly_web3/const.py,sha256=UYBs1b9P2YJw1isbUzlGOxMWIqWyRiyGt9D2WmZCYfU,4959
4
- poly_web3/log.py,sha256=NPY6X7Ysw-5ds3JkA26vhfge2Dc39ptezmCWYbwT-FM,986
5
- poly_web3/schema.py,sha256=GTzpEy8-mqw2fCqoQKQFn6ieyhXdSnW6I9YQMKGnnhM,435
6
- poly_web3/signature/__init__.py,sha256=p4EwohjUtwEsFqrc7eeqY5sQa14GWc8BdR6km8c7SH8,123
7
- poly_web3/signature/build.py,sha256=PEyMFbE2PM2Xvz3wvmA6T6FVnU3mB7uanoEutbneFQ8,3614
8
- poly_web3/signature/hash_message.py,sha256=hREtjuOdLcm_IlBz1YuzyrObVdxs7Yn-ULfh09PeNmk,1319
9
- poly_web3/signature/secp256k1.py,sha256=IBgp0vNJyskU7ZUcWCdDttrzmJgEWH6lakB74whur_w,1500
10
- poly_web3/web3_service/__init__.py,sha256=HNRh06yZNYXDmGS69ISXG7_PPG9foGXLyHOdnbgYXk0,315
11
- poly_web3/web3_service/base.py,sha256=SwiJ3nTPHUTYH5gDwjDPRoxjZNICfYKsy6w4pWiG73U,12195
12
- poly_web3/web3_service/eoa_service.py,sha256=mTJ0OHJvpzkrYvom1wDlA7EyV_tJFwxekpgPSXMXz0Y,515
13
- poly_web3/web3_service/proxy_service.py,sha256=-Odgpq24dKnBmtuwQ3RRINS88zzTFEQCbtaTZ4WNLs8,4181
14
- poly_web3/web3_service/safe_service.py,sha256=D9rHcxbU9rjvDd5k__qu8MCZ8srdQITS8g4JNkoE7Uo,776
15
- poly_web3-1.0.0.dist-info/METADATA,sha256=7r6FXHqXFPdPz8IpQHSEoI-Qm0-ZdrgVL1k_UiZf0fc,9765
16
- poly_web3-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- poly_web3-1.0.0.dist-info/top_level.txt,sha256=wW40wsocHfhFgqSWWvYWrhwKGQU5IWkhyaz5J90vmHo,19
18
- poly_web3-1.0.0.dist-info/RECORD,,