poly-web3 0.0.1__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.
@@ -0,0 +1,54 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025-12-27 17:01:20
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: example_redeem.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
+
14
+ from py_clob_client.client import ClobClient
15
+
16
+ from poly_web3 import RELAYER_URL, PolyWeb3Service
17
+
18
+ dotenv.load_dotenv()
19
+
20
+ if __name__ == "__main__":
21
+ host: str = "https://clob.polymarket.com"
22
+ chain_id: int = 137 # No need to adjust this
23
+ client = ClobClient(
24
+ host,
25
+ key=os.getenv("POLY_API_KEY"),
26
+ chain_id=chain_id,
27
+ signature_type=1,
28
+ funder=os.getenv("POLYMARKET_PROXY_ADDRESS"),
29
+ )
30
+ creds = client.create_or_derive_api_creds()
31
+ client.set_api_creds(creds)
32
+ relayer_client = RelayClient(
33
+ RELAYER_URL,
34
+ chain_id,
35
+ os.getenv("POLY_API_KEY"),
36
+ BuilderConfig(
37
+ local_builder_creds=BuilderApiKeyCreds(
38
+ key=os.getenv("BUILDER_KEY"),
39
+ secret=os.getenv("BUILDER_SECRET"),
40
+ passphrase=os.getenv("BUILDER_PASSPHRASE"),
41
+ )
42
+ ),
43
+ )
44
+ condition_id = "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c"
45
+ service = PolyWeb3Service(clob_client=client, relayer_client=relayer_client)
46
+ redeem = service.redeem(condition_id=condition_id)
47
+ print(redeem)
48
+
49
+ # Optional - Query operations (可选操作,用于查询)
50
+ # can_redeem = service.is_condition_resolved(condition_id)
51
+ # redeem_balance = service.get_redeemable_index_and_balance(
52
+ # condition_id, owner=client.builder.funder
53
+ # )
54
+ # print(can_redeem, redeem_balance)
poly_web3/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/19 15:24
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: __init__.py.py
6
+ # @Software: PyCharm
7
+ from typing import Union
8
+
9
+ from py_clob_client.client import ClobClient
10
+ from py_builder_relayer_client.client import RelayClient
11
+
12
+ from poly_web3.const import RELAYER_URL
13
+ from poly_web3.web3_service.base import BaseWeb3Service
14
+ from poly_web3.schema import WalletType
15
+ from poly_web3.web3_service import SafeWeb3Service, EOAWeb3Service, ProxyWeb3Service
16
+
17
+
18
+ def PolyWeb3Service(
19
+ clob_client: ClobClient, relayer_client: RelayClient = None
20
+ ) -> Union[SafeWeb3Service, EOAWeb3Service, ProxyWeb3Service]: # noqa
21
+ services = {
22
+ WalletType.EOA: EOAWeb3Service,
23
+ WalletType.PROXY: ProxyWeb3Service,
24
+ WalletType.SAFE: SafeWeb3Service,
25
+ }
26
+
27
+ wallet_type = WalletType.get_with_code(clob_client.builder.sig_type)
28
+ if service := services.get(wallet_type):
29
+ return service(clob_client, relayer_client)
30
+ else:
31
+ raise Exception(f"Unknown wallet type: {wallet_type}")
poly_web3/const.py ADDED
@@ -0,0 +1,171 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/19 15:29
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: const.py
6
+ # @Software: PyCharm
7
+ from web3 import Web3
8
+
9
+ GET_NONCE = "/nonce"
10
+ GET_RELAY_PAYLOAD = "/relay-payload"
11
+ GET_TRANSACTION = "/transaction"
12
+ GET_TRANSACTIONS = "/transactions"
13
+ SUBMIT_TRANSACTION = "/submit"
14
+ GET_DEPLOYED = "/deployed"
15
+ RPC_URL = "https://polygon-rpc.com"
16
+ RELAYER_URL = "https://relayer-v2.polymarket.com"
17
+
18
+ STATE_NEW = ("STATE_NEW",)
19
+ STATE_EXECUTED = "STATE_EXECUTED"
20
+ STATE_MINED = "STATE_MINED"
21
+ STATE_INVALID = "STATE_INVALID"
22
+ STATE_CONFIRMED = "STATE_CONFIRMED"
23
+ STATE_FAILED = "STATE_FAILED"
24
+
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(
29
+ "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
30
+ )
31
+ ZERO_BYTES32 = "0x" + "00" * 32
32
+ proxy_factory_address = Web3.to_checksum_address(
33
+ "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
34
+ )
35
+ SAFE_INIT_CODE_HASH = (
36
+ "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf"
37
+ )
38
+ PROXY_INIT_CODE_HASH = (
39
+ "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b"
40
+ )
41
+
42
+ AMOY = {
43
+ "ProxyContracts": {
44
+ # Proxy factory unsupported on Amoy testnet
45
+ "RelayHub": "",
46
+ "ProxyFactory": "",
47
+ },
48
+ "SafeContracts": {
49
+ "SafeFactory": "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b",
50
+ "SafeMultisend": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
51
+ },
52
+ }
53
+
54
+ POL = {
55
+ "ProxyContracts": {
56
+ "ProxyFactory": "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052",
57
+ "RelayHub": "0xD216153c06E857cD7f72665E0aF1d7D82172F494",
58
+ },
59
+ "SafeContracts": {
60
+ "SafeFactory": "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b",
61
+ "SafeMultisend": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
62
+ },
63
+ }
64
+
65
+ # abi
66
+ CTF_ABI_REDEEM = [
67
+ {
68
+ "name": "redeemPositions",
69
+ "type": "function",
70
+ "stateMutability": "nonpayable",
71
+ "inputs": [
72
+ {"name": "collateralToken", "type": "address"},
73
+ {"name": "parentCollectionId", "type": "bytes32"},
74
+ {"name": "conditionId", "type": "bytes32"},
75
+ {"name": "indexSets", "type": "uint256[]"},
76
+ ],
77
+ "outputs": [],
78
+ }
79
+ ]
80
+
81
+ NEG_RISK_ADAPTER_ABI_REDEEM = [
82
+ {
83
+ "name": "redeemPositions",
84
+ "type": "function",
85
+ "stateMutability": "nonpayable",
86
+ "inputs": [
87
+ {"name": "_conditionId", "type": "bytes32"},
88
+ {"name": "_amounts", "type": "uint256[]"},
89
+ ],
90
+ "outputs": [],
91
+ }
92
+ ]
93
+
94
+ proxy_wallet_factory_abi = [
95
+ {
96
+ "inputs": [
97
+ {
98
+ "components": [
99
+ {"name": "typeCode", "type": "uint8"},
100
+ {"name": "to", "type": "address"},
101
+ {"name": "value", "type": "uint256"},
102
+ {"name": "data", "type": "bytes"},
103
+ ],
104
+ "name": "calls",
105
+ "type": "tuple[]",
106
+ }
107
+ ],
108
+ "name": "proxy",
109
+ "outputs": [{"name": "returnValues", "type": "bytes[]"}],
110
+ "stateMutability": "payable",
111
+ "type": "function",
112
+ }
113
+ ]
114
+
115
+ CTF_ABI_PAYOUT = [
116
+ {
117
+ "name": "payoutDenominator",
118
+ "type": "function",
119
+ "stateMutability": "view",
120
+ "inputs": [{"name": "conditionId", "type": "bytes32"}],
121
+ "outputs": [{"type": "uint256"}],
122
+ },
123
+ {
124
+ "name": "payoutNumerators",
125
+ "type": "function",
126
+ "stateMutability": "view",
127
+ "inputs": [
128
+ {"name": "conditionId", "type": "bytes32"},
129
+ {"name": "index", "type": "uint256"},
130
+ ],
131
+ "outputs": [{"type": "uint256"}],
132
+ },
133
+ {
134
+ "name": "getOutcomeSlotCount",
135
+ "type": "function",
136
+ "stateMutability": "view",
137
+ "inputs": [{"name": "conditionId", "type": "bytes32"}],
138
+ "outputs": [{"type": "uint256"}],
139
+ },
140
+ {
141
+ "name": "getCollectionId",
142
+ "type": "function",
143
+ "stateMutability": "view",
144
+ "inputs": [
145
+ {"name": "parentCollectionId", "type": "bytes32"},
146
+ {"name": "conditionId", "type": "bytes32"},
147
+ {"name": "indexSet", "type": "uint256"},
148
+ ],
149
+ "outputs": [{"type": "bytes32"}],
150
+ },
151
+ {
152
+ "name": "getPositionId",
153
+ "type": "function",
154
+ "stateMutability": "view",
155
+ "inputs": [
156
+ {"name": "collateralToken", "type": "address"},
157
+ {"name": "collectionId", "type": "bytes32"},
158
+ ],
159
+ "outputs": [{"type": "uint256"}],
160
+ },
161
+ {
162
+ "name": "balanceOf",
163
+ "type": "function",
164
+ "stateMutability": "view",
165
+ "inputs": [
166
+ {"name": "owner", "type": "address"},
167
+ {"name": "id", "type": "uint256"},
168
+ ],
169
+ "outputs": [{"type": "uint256"}],
170
+ },
171
+ ]
poly_web3/schema.py ADDED
@@ -0,0 +1,22 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/19 15:31
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: model.py
6
+ # @Software: PyCharm
7
+ from enum import Enum
8
+
9
+
10
+ class WalletType(str, Enum):
11
+ EOA = "EOA"
12
+ SAFE = "SAFE"
13
+ PROXY = "PROXY"
14
+
15
+ @classmethod
16
+ def get_with_code(cls, code: int):
17
+ if code == 0:
18
+ return cls.EOA
19
+ elif code == 1:
20
+ return cls.PROXY
21
+ elif code == 2:
22
+ return cls.SAFE
@@ -0,0 +1,6 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/21 19:42
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: __init__.py.py
6
+ # @Software: PyCharm
@@ -0,0 +1,113 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/21 19:45
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: build.py
6
+ # @Software: PyCharm
7
+ from typing import Union, Optional
8
+
9
+
10
+ def string_to_bytes(value: str, size: Optional[int] = None) -> bytes:
11
+ data = value.encode("utf-8")
12
+ if size is None:
13
+ return data
14
+ if len(data) > size:
15
+ raise ValueError(f"Size overflow: given {len(data)}, max {size}")
16
+ return data + b"\x00" * (size - len(data))
17
+
18
+
19
+ def keccak256(data: bytes) -> bytes:
20
+ try:
21
+ from eth_hash.auto import keccak
22
+
23
+ return keccak(data)
24
+ except Exception:
25
+ try:
26
+ import sha3 # pysha3
27
+
28
+ k = sha3.keccak_256()
29
+ except Exception:
30
+ from Crypto.Hash import keccak # pycryptodome
31
+
32
+ k = keccak.new(digest_bits=256)
33
+ k.update(data)
34
+ return k.digest()
35
+
36
+
37
+ def to_checksum_address(addr_bytes: bytes) -> str:
38
+ hex_addr = addr_bytes.hex()
39
+ hash_hex = keccak256(hex_addr.encode()).hex()
40
+ checksum = "".join(
41
+ ch.upper() if int(hash_hex[i], 16) >= 8 else ch for i, ch in enumerate(hex_addr)
42
+ )
43
+ return "0x" + checksum
44
+
45
+
46
+ def derive_proxy_wallet(address: str, proxy_factory: str, bytecode_hash: str) -> str:
47
+ addr_bytes = bytes.fromhex(address[2:] if address.startswith("0x") else address)
48
+ factory_bytes = bytes.fromhex(
49
+ proxy_factory[2:] if proxy_factory.startswith("0x") else proxy_factory
50
+ )
51
+ bytecode_bytes = bytes.fromhex(
52
+ bytecode_hash[2:] if bytecode_hash.startswith("0x") else bytecode_hash
53
+ )
54
+ salt = keccak256(
55
+ addr_bytes
56
+ ) # equivalent to keccak256(encodePacked(["address"], [address]))
57
+ data = b"\xff" + factory_bytes + salt + bytecode_bytes
58
+ create2_hash = keccak256(data)
59
+ return to_checksum_address(create2_hash[12:]) # last 20 bytes
60
+
61
+
62
+ HexLike = Union[str, bytes, int]
63
+
64
+
65
+ def create_struct_hash(
66
+ from_addr: str,
67
+ to: str,
68
+ data: str,
69
+ tx_fee: HexLike,
70
+ gas_price: HexLike,
71
+ gas_limit: HexLike,
72
+ nonce: HexLike,
73
+ relay_hub_address: str,
74
+ relay_address: str,
75
+ ) -> str:
76
+ def to_bytes(hex_like: HexLike, size: int | None = None) -> bytes:
77
+ if isinstance(hex_like, int):
78
+ length = (
79
+ size if size is not None else max(1, (hex_like.bit_length() + 7) // 8)
80
+ )
81
+ return hex_like.to_bytes(length, "big")
82
+ if isinstance(hex_like, bytes):
83
+ return hex_like.rjust(size, b"\x00") if size else hex_like
84
+ if isinstance(hex_like, str):
85
+ if hex_like.startswith("0x"):
86
+ raw = bytes.fromhex(hex_like[2:])
87
+ return raw.rjust(size, b"\x00") if size else raw
88
+ if hex_like.isdigit(): # numeric string -> int
89
+ num = int(hex_like)
90
+ length = (
91
+ size if size is not None else max(1, (num.bit_length() + 7) // 8)
92
+ )
93
+ return num.to_bytes(length, "big")
94
+ raw = hex_like.encode() # fallback ascii
95
+ return raw.rjust(size, b"\x00") if size else raw
96
+ raise TypeError("Unsupported type for to_bytes")
97
+
98
+ relay_hub_prefix = to_bytes("rlx:")
99
+ data_to_hash = b"".join(
100
+ [
101
+ relay_hub_prefix,
102
+ to_bytes(from_addr),
103
+ to_bytes(to),
104
+ to_bytes(data),
105
+ to_bytes(tx_fee, size=32),
106
+ to_bytes(gas_price, size=32),
107
+ to_bytes(gas_limit, size=32),
108
+ to_bytes(nonce, size=32),
109
+ to_bytes(relay_hub_address),
110
+ to_bytes(relay_address),
111
+ ]
112
+ )
113
+ return "0x" + keccak256(data_to_hash).hex()
@@ -0,0 +1,48 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/21 19:43
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: hash_message.py
6
+ # @Software: PyCharm
7
+ from eth_hash.auto import keccak
8
+
9
+
10
+ def _is_hex(s: str) -> bool:
11
+ return isinstance(s, str) and s.startswith("0x")
12
+
13
+
14
+ def _hex_to_bytes(h: str) -> bytes:
15
+ # 假设是 0x 前缀
16
+ return bytes.fromhex(h[2:])
17
+
18
+
19
+ def _size_of_message(value) -> int:
20
+ # 对齐 viem: hex -> (len-2)/2 向上取整, bytes -> len
21
+ if _is_hex(value):
22
+ return (len(value) - 2 + 1) // 2
23
+ return len(value)
24
+
25
+
26
+ def _to_prefixed_message(message) -> bytes:
27
+ if isinstance(message, str):
28
+ msg_bytes = message.encode("utf-8")
29
+ elif isinstance(message, dict) and "raw" in message:
30
+ raw = message["raw"]
31
+ if isinstance(raw, str):
32
+ # viem 这里直接当作 hex 字符串使用
33
+ msg_bytes = _hex_to_bytes(raw)
34
+ else:
35
+ # raw 可以是 list[int], bytes, bytearray
36
+ msg_bytes = bytes(raw)
37
+ else:
38
+ raise TypeError("Unsupported SignableMessage")
39
+
40
+ prefix = f"\x19Ethereum Signed Message:\n{len(msg_bytes)}".encode("utf-8")
41
+ return prefix + msg_bytes
42
+
43
+
44
+ def hash_message(message, to: str = "hex"):
45
+ digest = keccak(_to_prefixed_message(message))
46
+ if to == "bytes":
47
+ return digest
48
+ return "0x" + digest.hex()
@@ -0,0 +1,57 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/21 19:44
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: secp256k1.py
6
+ # @Software: PyCharm
7
+ from eth_keys import keys
8
+ from eth_utils import decode_hex
9
+
10
+ SECP256K1_N = int(
11
+ "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16
12
+ )
13
+ HALF_N = SECP256K1_N // 2
14
+
15
+
16
+ def sign(hash_hex: str, priv_hex: str):
17
+ msg = decode_hex(hash_hex)
18
+ priv = keys.PrivateKey(decode_hex(priv_hex))
19
+ sig = priv.sign_msg_hash(msg)
20
+ r, s, v = sig.r, sig.s, sig.v
21
+
22
+ if s > HALF_N:
23
+ s = SECP256K1_N - s
24
+ # Ethereum recovery id flips when s is negated
25
+ v ^= 1
26
+
27
+ return r, s, v
28
+
29
+
30
+ def int_to_hex(n: int, size: int = 32) -> str:
31
+ # size 是字节数,32 字节 -> 64 个 hex 字符
32
+ return "0x" + n.to_bytes(size, "big").hex()
33
+
34
+
35
+ def hex_to_int(h: str) -> int:
36
+ return int(h, 16)
37
+
38
+
39
+ def serialize_signature(r: str, s: str, v=None, yParity=None, to="hex"):
40
+ # 计算 yParity_
41
+ if yParity in (0, 1):
42
+ yParity_ = yParity
43
+ elif v is not None and (v in (27, 28) or v >= 35):
44
+ yParity_ = 1 if (v % 2 == 0) else 0
45
+ else:
46
+ raise ValueError("Invalid `v` or `yParity` value")
47
+
48
+ # toCompactHex: r||s (各 32 字节)
49
+ r_int = hex_to_int(r)
50
+ s_int = hex_to_int(s)
51
+ compact = r_int.to_bytes(32, "big").hex() + s_int.to_bytes(32, "big").hex()
52
+
53
+ signature_hex = "0x" + compact + ("1b" if yParity_ == 0 else "1c")
54
+
55
+ if to == "hex":
56
+ return signature_hex
57
+ return bytes.fromhex(signature_hex[2:])
@@ -0,0 +1,9 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025-12-27 16:07:53
3
+ # @Author: PinBar
4
+ # @Site:
5
+ # @File: __init__.py
6
+ # @Software: PyCharm
7
+ from poly_web3.web3_service.eoa_service import EOAWeb3Service
8
+ from poly_web3.web3_service.proxy_service import ProxyWeb3Service
9
+ from poly_web3.web3_service.safe_service import SafeWeb3Service
@@ -0,0 +1,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
+ ):
35
+ self.relayer_client = relayer_client
36
+ self.clob_client: ClobClient = clob_client
37
+ if self.clob_client:
38
+ self.wallet_type: WalletType = WalletType.get_with_code(
39
+ self.clob_client.builder.sig_type
40
+ )
41
+ else:
42
+ self.wallet_type = WalletType.PROXY
43
+ self.w3: Web3 = Web3(Web3.HTTPProvider(RPC_URL))
44
+ if self.wallet_type == WalletType.PROXY and relayer_client is None:
45
+ raise Exception("relayer_client must be provided")
46
+
47
+ def is_condition_resolved(self, condition_id: str) -> bool:
48
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
49
+ return ctf.functions.payoutDenominator(condition_id).call() > 0
50
+
51
+ def get_winning_indexes(self, condition_id: str) -> list[int]:
52
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
53
+ if not self.is_condition_resolved(condition_id):
54
+ return []
55
+ outcome_count = ctf.functions.getOutcomeSlotCount(condition_id).call()
56
+ winners: list[int] = []
57
+ for i in range(outcome_count):
58
+ if ctf.functions.payoutNumerators(condition_id, i).call() > 0:
59
+ winners.append(i)
60
+ return winners
61
+
62
+ def get_redeemable_index_and_balance(
63
+ self, condition_id: str, owner: str
64
+ ) -> list[tuple]:
65
+ winners = self.get_winning_indexes(condition_id)
66
+ if not winners:
67
+ return []
68
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_PAYOUT)
69
+ owner_checksum = Web3.to_checksum_address(owner)
70
+ redeemable: list[tuple] = []
71
+ for index in winners:
72
+ index_set = 1 << index
73
+ collection_id = ctf.functions.getCollectionId(
74
+ ZERO_BYTES32, condition_id, index_set
75
+ ).call()
76
+ position_id = ctf.functions.getPositionId(
77
+ USDC_POLYGON, collection_id
78
+ ).call()
79
+ balance = ctf.functions.balanceOf(owner_checksum, position_id).call()
80
+ if balance > 0:
81
+ redeemable.append((index, balance / 1000000))
82
+ return redeemable
83
+
84
+ def build_ctf_redeem_tx_data(self, condition_id: str) -> str:
85
+ ctf = self.w3.eth.contract(address=CTF_ADDRESS, abi=CTF_ABI_REDEEM)
86
+ # 只需要 calldata:encodeABI 即可
87
+ return ctf.functions.redeemPositions(
88
+ USDC_POLYGON,
89
+ ZERO_BYTES32,
90
+ condition_id,
91
+ [1, 2],
92
+ )._encode_transaction_data()
93
+
94
+ def build_neg_risk_redeem_tx_data(
95
+ self, condition_id: str, redeem_amounts: list[int]
96
+ ) -> str:
97
+ nr_adapter = self.w3.eth.contract(
98
+ address=NEG_RISK_ADAPTER_ADDRESS, abi=NEG_RISK_ADAPTER_ABI_REDEEM
99
+ )
100
+ return nr_adapter.functions.redeemPositions(
101
+ condition_id,
102
+ redeem_amounts,
103
+ )._encode_transaction_data()
104
+
105
+ @classmethod
106
+ def _get_relay_payload(cls, address: str, wallet_type: WalletType):
107
+ return requests.get(
108
+ RELAYER_URL + GET_RELAY_PAYLOAD,
109
+ params={"address": address, "type": wallet_type},
110
+ ).json()
111
+
112
+ def get_contract_config(self) -> dict:
113
+ if self.clob_client.chain_id == 137:
114
+ return POL
115
+ elif self.clob_client.chain_id == 80002:
116
+ return AMOY
117
+ raise Exception("Invalid network")
118
+
119
+ def estimate_gas(self, tx):
120
+ payload = {
121
+ "jsonrpc": "2.0",
122
+ "method": "eth_estimateGas",
123
+ "params": [tx],
124
+ "id": 1,
125
+ }
126
+
127
+ response = requests.post(RPC_URL, json=payload)
128
+ result = response.json()
129
+
130
+ if "result" in result:
131
+ # 返回的是16进制 gas 数量
132
+ gas_hex = result["result"]
133
+ return str(int(gas_hex, 16))
134
+ else:
135
+ raise Exception("Estimate gas error: " + str(result))
136
+
137
+ def redeem(
138
+ self,
139
+ condition_id: str,
140
+ neg_risk: bool = False,
141
+ redeem_amounts: list[int] | None = None,
142
+ ): # noqa:
143
+ raise ImportError()
@@ -0,0 +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")
@@ -0,0 +1,135 @@
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
@@ -0,0 +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")
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: poly-web3
3
+ Version: 0.0.1
4
+ Summary: Polymarket Proxy wallet redeem SDK - Execute redeem operations on Polymarket using proxy wallets
5
+ Home-page: https://github.com/tosmart01/poly-web3
6
+ Author: PinBar
7
+ Project-URL: Homepage, https://github.com/tosmart01/poly-web3
8
+ Project-URL: Repository, https://github.com/tosmart01/poly-web3
9
+ Project-URL: Bug Tracker, https://github.com/tosmart01/poly-web3/issues
10
+ Keywords: polymarket,web3,proxy,wallet,redeem,blockchain,polygon
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: py-clob-client>=0.25.0
21
+ Requires-Dist: py-builder-relayer-client>=0.0.1
22
+ Requires-Dist: web3==6.8
23
+ Requires-Dist: eth-utils==5.3.1
24
+ Requires-Dist: setuptools>=80.9.0
25
+ Dynamic: home-page
26
+ Dynamic: requires-python
27
+
28
+ # poly-web3
29
+
30
+ Python SDK for Polymarket Proxy wallet redeem operations. Supports executing Conditional Token Fund (CTF) redeem operations on Polymarket through proxy wallets.
31
+
32
+ [English](README.md) | [中文](README.zh.md)
33
+
34
+ ## About the Project
35
+
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.
37
+
38
+ **Important Notes:**
39
+ - This project **only implements the official redeem functionality**, focusing on Conditional Token Fund (CTF) redeem operations
40
+ - Other features (such as trading, order placement, etc.) are not within the scope of this project
41
+
42
+ **Current Status:**
43
+ - ✅ **Proxy Wallet** - Fully supported for redeem functionality
44
+ - 🚧 **Safe Wallet** - Under development
45
+ - 🚧 **EOA Wallet** - Under development
46
+
47
+ 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.
48
+
49
+ ## Features
50
+
51
+ - ✅ Support for Polymarket Proxy wallet redeem operations (currently only Proxy wallet is supported)
52
+ - ✅ Check if conditions are resolved
53
+ - ✅ Get redeemable indexes and balances
54
+ - ✅ Support for standard CTF redeem and negative risk (neg_risk) redeem
55
+ - ✅ Automatic transaction execution through Relayer service
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install poly-web3
61
+ ```
62
+
63
+ Or using uv:
64
+
65
+ ```bash
66
+ uv add poly-web3
67
+ ```
68
+
69
+ ## Requirements
70
+
71
+ - Python >= 3.11
72
+
73
+ ## Dependencies
74
+
75
+ - `py-clob-client >= 0.25.0` - Polymarket CLOB client
76
+ - `py-builder-relayer-client >= 0.0.1` - Builder Relayer client
77
+ - `web3 == 6.8` - Web3.py library
78
+ - `eth-utils == 5.3.1` - Ethereum utilities library
79
+
80
+ ## Quick Start
81
+
82
+ ### Basic Usage - Execute Redeem
83
+
84
+ ```python
85
+ import os
86
+ import dotenv
87
+ from py_builder_relayer_client.client import RelayClient
88
+ from py_builder_signing_sdk.config import BuilderConfig
89
+ from py_builder_signing_sdk.sdk_types import BuilderApiKeyCreds
90
+ from py_clob_client.client import ClobClient
91
+ from poly_web3 import RELAYER_URL, PolyWeb3Service
92
+
93
+ dotenv.load_dotenv()
94
+
95
+ # Initialize ClobClient
96
+ host = "https://clob.polymarket.com"
97
+ chain_id = 137 # Polygon mainnet
98
+ client = ClobClient(
99
+ host,
100
+ key=os.getenv("POLY_API_KEY"),
101
+ chain_id=chain_id,
102
+ signature_type=1, # Proxy wallet type
103
+ funder=os.getenv("POLYMARKET_PROXY_ADDRESS"),
104
+ )
105
+
106
+ client.set_api_creds(client.create_or_derive_api_creds())
107
+
108
+ # Initialize RelayerClient
109
+ relayer_client = RelayClient(
110
+ RELAYER_URL,
111
+ chain_id,
112
+ os.getenv("POLY_API_KEY"),
113
+ BuilderConfig(
114
+ local_builder_creds=BuilderApiKeyCreds(
115
+ key=os.getenv("BUILDER_KEY"),
116
+ secret=os.getenv("BUILDER_SECRET"),
117
+ passphrase=os.getenv("BUILDER_PASSPHRASE"),
118
+ )
119
+ ),
120
+ )
121
+
122
+ # Create service instance
123
+ service = PolyWeb3Service(clob_client=client, relayer_client=relayer_client)
124
+
125
+ # Execute redeem operation
126
+ condition_id = "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c"
127
+ redeem_result = service.redeem(condition_id=condition_id)
128
+ print(f"Redeem result: {redeem_result}")
129
+ ```
130
+
131
+ ### Optional - Query Operations
132
+
133
+ Before executing redeem, you can optionally check the condition status and query redeemable balances:
134
+
135
+ ```python
136
+ # Check if condition is resolved
137
+ condition_id = "0xc3df016175463c44f9c9f98bddaa3bf3daaabb14b069fb7869621cffe73ddd1c"
138
+ can_redeem = service.is_condition_resolved(condition_id)
139
+
140
+ # Get redeemable indexes and balances
141
+ redeem_balance = service.get_redeemable_index_and_balance(
142
+ condition_id, owner=client.builder.funder
143
+ )
144
+
145
+ print(f"Can redeem: {can_redeem}")
146
+ print(f"Redeemable balance: {redeem_balance}")
147
+ ```
148
+
149
+ ## API Documentation
150
+
151
+ ### PolyWeb3Service
152
+
153
+ The main service class that automatically selects the appropriate service implementation based on wallet type.
154
+
155
+ #### Methods
156
+
157
+ ##### `is_condition_resolved(condition_id: str) -> bool`
158
+
159
+ Check if the specified condition is resolved.
160
+
161
+ **Parameters:**
162
+ - `condition_id` (str): Condition ID (32-byte hexadecimal string)
163
+
164
+ **Returns:**
165
+ - `bool`: Returns `True` if the condition is resolved, otherwise `False`
166
+
167
+ ##### `get_winning_indexes(condition_id: str) -> list[int]`
168
+
169
+ Get the list of winning indexes.
170
+
171
+ **Parameters:**
172
+ - `condition_id` (str): Condition ID
173
+
174
+ **Returns:**
175
+ - `list[int]`: List of winning indexes
176
+
177
+ ##### `get_redeemable_index_and_balance(condition_id: str, owner: str) -> list[tuple]`
178
+
179
+ Get redeemable indexes and balances for the specified address.
180
+
181
+ **Parameters:**
182
+ - `condition_id` (str): Condition ID
183
+ - `owner` (str): Wallet address
184
+
185
+ **Returns:**
186
+ - `list[tuple]`: List of tuples containing (index, balance), balance is in USDC units
187
+
188
+ ##### `redeem(condition_id: str, neg_risk: bool = False, redeem_amounts: list[int] | None = None)`
189
+
190
+ Execute redeem operation.
191
+
192
+ **Parameters:**
193
+ - `condition_id` (str): Condition ID
194
+ - `neg_risk` (bool): Whether it's a negative risk redeem, defaults to `False`
195
+ - `redeem_amounts` (list[int] | None): Amount list for negative risk redeem, must contain 2 elements
196
+
197
+ **Returns:**
198
+ - `dict`: Transaction result containing transaction status and related information
199
+
200
+ **Examples:**
201
+
202
+ ```python
203
+ # Standard CTF redeem
204
+ result = service.redeem(condition_id="0x...")
205
+
206
+ # Negative risk redeem
207
+ result = service.redeem(
208
+ condition_id="0x...",
209
+ neg_risk=True,
210
+ redeem_amounts=[1000000, 2000000] # Amounts in smallest unit (6 decimal places)
211
+ )
212
+ ```
213
+
214
+ ## Project Structure
215
+
216
+ ```
217
+ poly_web3/
218
+ ├── __init__.py # Main entry point, exports PolyWeb3Service
219
+ ├── const.py # Constant definitions (contract addresses, ABIs, etc.)
220
+ ├── schema.py # Data models (WalletType, etc.)
221
+ ├── signature/ # Signature-related modules
222
+ │ ├── build.py # Proxy wallet derivation and struct hashing
223
+ │ ├── hash_message.py # Message hashing
224
+ │ └── secp256k1.py # secp256k1 signing
225
+ └── web3_service/ # Web3 service implementations
226
+ ├── base.py # Base service class
227
+ ├── proxy_service.py # Proxy wallet service (✅ Implemented)
228
+ ├── eoa_service.py # EOA wallet service (🚧 Under development)
229
+ └── safe_service.py # Safe wallet service (🚧 Under development)
230
+ ```
231
+
232
+ ## Notes
233
+
234
+ 1. **Environment Variable Security**: Make sure `.env` file is added to `.gitignore`, do not commit sensitive information to the code repository
235
+ 2. **Network Support**: Currently mainly supports Polygon mainnet (chain_id: 137), Amoy testnet may have limited functionality
236
+ 3. **Wallet Type**: **Currently only Proxy wallet is supported** (signature_type: 1), Safe and EOA wallet redeem functionality is under development
237
+ 4. **Gas Fees**: Transactions are executed through Relayer, gas fees are handled by the Relayer
238
+
239
+ ## Development
240
+
241
+ ### Install Development Dependencies
242
+
243
+ ```bash
244
+ uv pip install -e ".[dev]"
245
+ ```
246
+
247
+ ### Run Examples
248
+
249
+ ```bash
250
+ python examples/example_redeem.py
251
+ ```
252
+
253
+ ### Contributing
254
+
255
+ We welcome all forms of contributions! If you'd like to:
256
+
257
+ - Implement Safe or EOA wallet support
258
+ - Fix bugs or improve existing functionality
259
+ - Add new features or improve documentation
260
+ - Make suggestions or report issues
261
+
262
+ Please feel free to submit an Issue or Pull Request. Your contributions will help make this project better!
263
+
264
+ ## License
265
+
266
+ MIT
267
+
268
+ ## Author
269
+
270
+ PinBar
271
+
272
+ ## Related Links
273
+
274
+ - [Polymarket](https://polymarket.com/)
275
+ - [Polygon Network](https://polygon.technology/)
@@ -0,0 +1,17 @@
1
+ examples/example_redeem.py,sha256=2KxsDjEtFrm9eo8Fme0CVAQ3NDu24G0Gfgwk4GdbzSo,1807
2
+ poly_web3/__init__.py,sha256=sOPcaBjGctd_wT-UjztKq8rvcYBRA_HGhCr7rLXJOPM,1055
3
+ poly_web3/const.py,sha256=VhH-UdsHN2DO993f60vnO093vSHAbaDwd6UIxb-GMuI,5091
4
+ poly_web3/schema.py,sha256=sxZOzYglLLyQLwbSlQGpQzQ3IB7R059XQzJAO9T5frg,457
5
+ poly_web3/signature/__init__.py,sha256=g5z1cPO_o-LPdvKb3VQu4aEcpJMMjJW-a9h9Mo6qczo,129
6
+ poly_web3/signature/build.py,sha256=iSOzrMQOte6NWA981gQYPkCtRSoXhlFEcmV0bI7XMrA,3727
7
+ poly_web3/signature/hash_message.py,sha256=IYCtqdV9HKE6bG6DZXt3wedrrXUn8KQ4RBgcqZq45aI,1367
8
+ poly_web3/signature/secp256k1.py,sha256=AGpItsDFTAJR9ZpAfyC26llKaNmIfpD8v6HM3qqMAIA,1557
9
+ poly_web3/web3_service/__init__.py,sha256=pxW7XqlSDZeoYc3ohwCcdtd_9aK3gJBIQ2ANXwHXUcE,324
10
+ poly_web3/web3_service/base.py,sha256=5lfTKubdazwSNcG1o6Q0oTM6JM4-kgtzWkfohR3dBkI,4909
11
+ poly_web3/web3_service/eoa_service.py,sha256=MmwaAmpD_WLk4M-2w99_F7iHTZ-pKtAnPw4m5x2h6CA,446
12
+ poly_web3/web3_service/proxy_service.py,sha256=qIhBqa8ESSTH0e5W9WVvqLPVpJpEM5sYeRLrkHLvC1Y,4760
13
+ poly_web3/web3_service/safe_service.py,sha256=KPmToVjqxFqFhFrDbY0hfR3Z00Alu9w31QKMyyluzX8,449
14
+ poly_web3-0.0.1.dist-info/METADATA,sha256=3hej2oW7wH7z9KGMAEPW2UrmnirviY6ra__cJjbgAMI,8914
15
+ poly_web3-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ poly_web3-0.0.1.dist-info/top_level.txt,sha256=wW40wsocHfhFgqSWWvYWrhwKGQU5IWkhyaz5J90vmHo,19
17
+ poly_web3-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ examples
2
+ poly_web3