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.
- examples/example_redeem.py +54 -0
- poly_web3/__init__.py +31 -0
- poly_web3/const.py +171 -0
- poly_web3/schema.py +22 -0
- poly_web3/signature/__init__.py +6 -0
- poly_web3/signature/build.py +113 -0
- poly_web3/signature/hash_message.py +48 -0
- poly_web3/signature/secp256k1.py +57 -0
- poly_web3/web3_service/__init__.py +9 -0
- poly_web3/web3_service/base.py +143 -0
- poly_web3/web3_service/eoa_service.py +17 -0
- poly_web3/web3_service/proxy_service.py +135 -0
- poly_web3/web3_service/safe_service.py +17 -0
- poly_web3-0.0.1.dist-info/METADATA +275 -0
- poly_web3-0.0.1.dist-info/RECORD +17 -0
- poly_web3-0.0.1.dist-info/WHEEL +5 -0
- poly_web3-0.0.1.dist-info/top_level.txt +2 -0
|
@@ -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,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,,
|