opinion-clob-sdk 0.1.1__py3-none-any.whl → 0.1.3__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.
Potentially problematic release.
This version of opinion-clob-sdk might be problematic. Click here for more details.
- opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/conditional_tokens.py +707 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/base_builder.py +41 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/exception.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder.py +90 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder_test.py +40 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/constants.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order.py +254 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order_type.py +9 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/sides.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/signatures.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/signer.py +20 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +109 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/eip712/__init__.py +176 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/compatibility_fallback_handler_v1_3_0.py +327 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/multisend_v1_3_0.py +22 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/safe_v1_3_0.py +1035 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/utils.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_signature.py +364 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_test.py +37 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_tx.py +437 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
- opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
- opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/conditional_tokens.py +707 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/base_builder.py +41 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/exception.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder.py +90 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder_test.py +40 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/constants.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order.py +254 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order_type.py +9 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/sides.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/signatures.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/signer.py +20 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +109 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/eip712/__init__.py +176 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/compatibility_fallback_handler_v1_3_0.py +327 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/multisend_v1_3_0.py +22 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/safe_v1_3_0.py +1035 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/utils.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_signature.py +364 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_test.py +37 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_tx.py +437 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/sdk.py +947 -0
- opinion_clob_sdk/opinion_clob_sdk/sdk.py +947 -0
- opinion_clob_sdk/sdk.py +24 -17
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/METADATA +1 -1
- opinion_clob_sdk-0.1.3.dist-info/RECORD +130 -0
- opinion_clob_sdk-0.1.1.dist-info/RECORD +0 -46
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/WHEEL +0 -0
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
import eth_abi
|
|
6
|
+
from eth_account import Account
|
|
7
|
+
from eth_typing import Address, AnyAddress, ChecksumAddress, Hash32, HexAddress, HexStr
|
|
8
|
+
from eth_utils import to_normalized_address
|
|
9
|
+
from hexbytes import HexBytes
|
|
10
|
+
# from sha3 import keccak_256
|
|
11
|
+
from web3.types import TxParams, Wei
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_empty_tx_params() -> TxParams:
|
|
15
|
+
"""
|
|
16
|
+
:return: Empty tx params, so calls like `build_transaction` don't call the RPC trying to get information
|
|
17
|
+
"""
|
|
18
|
+
return {
|
|
19
|
+
"gas": Wei(1),
|
|
20
|
+
"gasPrice": Wei(1),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=int(os.getenv("CACHE_KECCAK", 512)))
|
|
25
|
+
def _keccak_256(value: bytes) -> bytes:
|
|
26
|
+
import web3
|
|
27
|
+
return web3.Web3.keccak(value)
|
|
28
|
+
# return keccak_256(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fast_keccak(value: bytes) -> Hash32:
|
|
32
|
+
"""
|
|
33
|
+
Calculates ethereum keccak256 using fast library `pysha3`
|
|
34
|
+
|
|
35
|
+
:param value:
|
|
36
|
+
:return: Keccak256 used by ethereum as `HexBytes`
|
|
37
|
+
"""
|
|
38
|
+
return Hash32(HexBytes(_keccak_256(value)))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fast_keccak_text(value: str) -> Hash32:
|
|
42
|
+
"""
|
|
43
|
+
Calculates ethereum keccak256 using fast library `pysha3`
|
|
44
|
+
|
|
45
|
+
:param value:
|
|
46
|
+
:return: Keccak256 used by ethereum as `HexBytes`
|
|
47
|
+
"""
|
|
48
|
+
return fast_keccak(value.encode())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fast_keccak_hex(value: bytes) -> HexStr:
|
|
52
|
+
"""
|
|
53
|
+
Same as `fast_keccak`, but it's a little more optimal calling `hexdigest()`
|
|
54
|
+
than calling `digest()` and then `hex()`
|
|
55
|
+
|
|
56
|
+
:param value:
|
|
57
|
+
:return: Keccak256 used by ethereum as a hex string (not 0x prefixed)
|
|
58
|
+
"""
|
|
59
|
+
return HexStr(_keccak_256(value).hex())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_checksum_address(
|
|
63
|
+
norm_address: HexStr, address_hash: HexStr
|
|
64
|
+
) -> ChecksumAddress:
|
|
65
|
+
"""
|
|
66
|
+
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
|
|
67
|
+
|
|
68
|
+
:param norm_address: address in lowercase (not 0x prefixed)
|
|
69
|
+
:param address_hash: keccak256 of `norm_address` (not 0x prefixed)
|
|
70
|
+
:return:
|
|
71
|
+
"""
|
|
72
|
+
return ChecksumAddress(
|
|
73
|
+
HexAddress(
|
|
74
|
+
HexStr(
|
|
75
|
+
"0x"
|
|
76
|
+
+ (
|
|
77
|
+
"".join(
|
|
78
|
+
(
|
|
79
|
+
norm_address[i].upper()
|
|
80
|
+
if int(str(address_hash[i]), 16) > 7
|
|
81
|
+
else norm_address[i]
|
|
82
|
+
)
|
|
83
|
+
for i in range(0, 40)
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=int(os.getenv("CACHE_CHECKSUM_ADDRESS", 1_000_000_000)))
|
|
92
|
+
def _fast_to_checksum_address(address: HexAddress):
|
|
93
|
+
# print("_fast_to_checksum_address: {}".format(address))
|
|
94
|
+
address_hash = fast_keccak_hex(address.encode())
|
|
95
|
+
return _build_checksum_address(address, address_hash)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fast_to_checksum_address(value: Union[AnyAddress, str, bytes]) -> ChecksumAddress:
|
|
99
|
+
"""
|
|
100
|
+
Converts to checksum_address. Uses more optimal `pysha3` instead of `eth_utils` for keccak256 calculation
|
|
101
|
+
|
|
102
|
+
:param value:
|
|
103
|
+
:return:
|
|
104
|
+
"""
|
|
105
|
+
# print("fast_to_checksum_address: {}".format(value))
|
|
106
|
+
if isinstance(value, bytes):
|
|
107
|
+
if len(value) != 20:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"Cannot convert %s to a checksum address, 20 bytes were expected"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
norm_address = HexAddress(HexStr(to_normalized_address(value)[2:]))
|
|
113
|
+
return _fast_to_checksum_address(norm_address)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def fast_bytes_to_checksum_address(value: bytes) -> ChecksumAddress:
|
|
117
|
+
"""
|
|
118
|
+
Converts to checksum_address. Uses more optimal `pysha3` instead of `eth_utils` for keccak256 calculation.
|
|
119
|
+
As input is already in bytes, some checks and conversions can be skipped, providing a speedup of ~50%
|
|
120
|
+
|
|
121
|
+
:param value:
|
|
122
|
+
:return:
|
|
123
|
+
"""
|
|
124
|
+
if len(value) != 20:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"Cannot convert %s to a checksum address, 20 bytes were expected"
|
|
127
|
+
)
|
|
128
|
+
norm_address = HexAddress(HexStr(bytes(value).hex()))
|
|
129
|
+
return _fast_to_checksum_address(norm_address)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def fast_is_checksum_address(value: Union[AnyAddress, str, bytes]) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Fast version to check if an address is a checksum_address
|
|
135
|
+
|
|
136
|
+
:param value:
|
|
137
|
+
:return: `True` if checksummed, `False` otherwise
|
|
138
|
+
"""
|
|
139
|
+
if not isinstance(value, str) or len(value) != 42 or not value.startswith("0x"):
|
|
140
|
+
return False
|
|
141
|
+
try:
|
|
142
|
+
return fast_to_checksum_address(value) == value
|
|
143
|
+
except ValueError:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_eth_address_with_invalid_checksum() -> str:
|
|
148
|
+
address = Account.create().address
|
|
149
|
+
return "0x" + "".join(
|
|
150
|
+
[c.lower() if c.isupper() else c.upper() for c in address[2:]]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def decode_string_or_bytes32(data: bytes) -> str:
|
|
155
|
+
try:
|
|
156
|
+
return eth_abi.decode(["string"], data)[0]
|
|
157
|
+
except (OverflowError, eth_abi.exceptions.DecodingError):
|
|
158
|
+
name = eth_abi.decode(["bytes32"], data)[0]
|
|
159
|
+
end_position = name.find(b"\x00")
|
|
160
|
+
if end_position == -1:
|
|
161
|
+
return name.decode()
|
|
162
|
+
else:
|
|
163
|
+
return name[:end_position].decode()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def remove_swarm_metadata(code: bytes) -> bytes:
|
|
167
|
+
"""
|
|
168
|
+
Remove swarm metadata from Solidity bytecode
|
|
169
|
+
|
|
170
|
+
:param code:
|
|
171
|
+
:return: Code without metadata
|
|
172
|
+
"""
|
|
173
|
+
swarm = b"\xa1\x65bzzr0"
|
|
174
|
+
position = code.rfind(swarm)
|
|
175
|
+
if position == -1:
|
|
176
|
+
raise ValueError("Swarm metadata not found in code %s" % code.hex())
|
|
177
|
+
return code[:position]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def compare_byte_code(code_1: bytes, code_2: bytes) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Compare code, removing swarm metadata if necessary
|
|
183
|
+
|
|
184
|
+
:param code_1:
|
|
185
|
+
:param code_2:
|
|
186
|
+
:return: True if same code, False otherwise
|
|
187
|
+
"""
|
|
188
|
+
if code_1 == code_2:
|
|
189
|
+
return True
|
|
190
|
+
else:
|
|
191
|
+
codes = []
|
|
192
|
+
for code in (code_1, code_2):
|
|
193
|
+
try:
|
|
194
|
+
codes.append(remove_swarm_metadata(code))
|
|
195
|
+
except ValueError:
|
|
196
|
+
codes.append(code)
|
|
197
|
+
|
|
198
|
+
return codes[0] == codes[1]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def bytes_to_float(value: Any) -> float:
|
|
202
|
+
"""
|
|
203
|
+
Convert a value of type Any to float.
|
|
204
|
+
|
|
205
|
+
:param value: The value to convert.
|
|
206
|
+
:return: The converted float value.
|
|
207
|
+
:raises ValueError: If the value cannot be converted to float.
|
|
208
|
+
"""
|
|
209
|
+
assert value is not None, "Cannot convert None to float"
|
|
210
|
+
if isinstance(value, (int, float)):
|
|
211
|
+
return float(value)
|
|
212
|
+
elif isinstance(value, bytes):
|
|
213
|
+
try:
|
|
214
|
+
return float(int.from_bytes(value, "big"))
|
|
215
|
+
except (ValueError, OverflowError) as e:
|
|
216
|
+
raise ValueError(f"Cannot convert bytes to float: {e}")
|
|
217
|
+
else:
|
|
218
|
+
raise ValueError(f"Unsupported type for conversion to float: {type(value)}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TopicStatus(Enum):
|
|
5
|
+
CREATED = 1
|
|
6
|
+
ACTIVATED = 2
|
|
7
|
+
RESOLVING = 3
|
|
8
|
+
RESOLVED = 4
|
|
9
|
+
FAILED = 5
|
|
10
|
+
DELETED = 6
|
|
11
|
+
|
|
12
|
+
class TopicType(Enum):
|
|
13
|
+
CATEGORICAL = 1
|
|
14
|
+
BINARY = 0
|
|
15
|
+
|
|
16
|
+
class TopicStatusFilter(Enum):
|
|
17
|
+
ALL = 0
|
|
18
|
+
ACTIVATED = 2
|
|
19
|
+
RESOLVED = 4
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Opinion CLOB SDK - Python SDK for Opinion Prediction Market CLOB API"""
|
|
2
|
+
|
|
3
|
+
from opinion_clob_sdk.sdk import (
|
|
4
|
+
Client,
|
|
5
|
+
CHAIN_ID_BASE_MAINNET,
|
|
6
|
+
SUPPORTED_CHAIN_IDS
|
|
7
|
+
)
|
|
8
|
+
from opinion_clob_sdk.model import TopicStatus, TopicType, TopicStatusFilter
|
|
9
|
+
from opinion_clob_sdk.chain.exception import (
|
|
10
|
+
BalanceNotEnough,
|
|
11
|
+
NoPositionsToRedeem,
|
|
12
|
+
InsufficientGasBalance
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.3"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Client",
|
|
18
|
+
"TopicStatus",
|
|
19
|
+
"TopicType",
|
|
20
|
+
"TopicStatusFilter",
|
|
21
|
+
"CHAIN_ID_BASE_MAINNET",
|
|
22
|
+
"SUPPORTED_CHAIN_IDS",
|
|
23
|
+
"BalanceNotEnough",
|
|
24
|
+
"NoPositionsToRedeem",
|
|
25
|
+
"InsufficientGasBalance"
|
|
26
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from typing import List, Any
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from eth_typing import HexStr, ChecksumAddress, Hash32
|
|
6
|
+
from hexbytes import HexBytes
|
|
7
|
+
from web3 import Web3
|
|
8
|
+
from web3.contract import Contract
|
|
9
|
+
from web3.providers import HTTPProvider
|
|
10
|
+
|
|
11
|
+
from .exception import BalanceNotEnough, NoPositionsToRedeem, InsufficientGasBalance
|
|
12
|
+
from .safe.constants import NULL_HASH
|
|
13
|
+
from .safe.multisend import MultiSendTx, MultiSendOperation
|
|
14
|
+
from .safe.safe import Safe
|
|
15
|
+
from .py_order_utils.signer import Signer
|
|
16
|
+
from .safe.utils import get_empty_tx_params
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContractCaller:
|
|
20
|
+
def __init__(self, rpc_url='', private_key: HexStr = '', multi_sig_addr: ChecksumAddress = '',
|
|
21
|
+
conditional_tokens_addr: ChecksumAddress = '', multisend_addr: ChecksumAddress = '',
|
|
22
|
+
enable_trading_check_interval=3600):
|
|
23
|
+
"""
|
|
24
|
+
Initialize ContractCaller for blockchain interactions.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
rpc_url: RPC endpoint URL
|
|
28
|
+
private_key: Private key for signing transactions
|
|
29
|
+
multi_sig_addr: Multi-signature wallet address
|
|
30
|
+
conditional_tokens_addr: Conditional tokens contract address
|
|
31
|
+
multisend_addr: Multisend contract address
|
|
32
|
+
enable_trading_check_interval: Time interval (in seconds) to cache enable_trading checks.
|
|
33
|
+
Default is 3600 (1 hour). Within this interval, enable_trading() will return
|
|
34
|
+
immediately without checking blockchain state, improving performance significantly.
|
|
35
|
+
"""
|
|
36
|
+
self.private_key = private_key
|
|
37
|
+
self.signer = Signer(self.private_key)
|
|
38
|
+
|
|
39
|
+
self.multi_sig_addr = multi_sig_addr
|
|
40
|
+
self.conditional_tokens_addr = conditional_tokens_addr
|
|
41
|
+
self.multisend_addr = multisend_addr
|
|
42
|
+
w3 = Web3(HTTPProvider(rpc_url))
|
|
43
|
+
self.w3 = w3
|
|
44
|
+
self.safe = Safe(w3, private_key, multi_sig_addr, multisend_addr)
|
|
45
|
+
self.__enable_trading_check_interval: int = enable_trading_check_interval
|
|
46
|
+
self.__enable_trading_last_time: float = None
|
|
47
|
+
# Cache for token decimals to avoid repeated contract calls
|
|
48
|
+
self._token_decimals_cache: dict = {}
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def conditional_tokens(self) -> Contract:
|
|
52
|
+
from .contracts.conditional_tokens import abi
|
|
53
|
+
return self.w3.eth.contract(self.conditional_tokens_addr, abi=abi)
|
|
54
|
+
|
|
55
|
+
def get_erc20_contract(self, address: ChecksumAddress):
|
|
56
|
+
from .contracts.erc20 import abi
|
|
57
|
+
return self.w3.eth.contract(address, abi=abi)
|
|
58
|
+
|
|
59
|
+
def get_token_decimals(self, token_address: ChecksumAddress) -> int:
|
|
60
|
+
"""Get token decimals with caching to avoid repeated contract calls"""
|
|
61
|
+
token_key = token_address.lower()
|
|
62
|
+
|
|
63
|
+
if token_key not in self._token_decimals_cache:
|
|
64
|
+
erc20_contract = self.get_erc20_contract(token_address)
|
|
65
|
+
try:
|
|
66
|
+
decimals = erc20_contract.functions.decimals().call()
|
|
67
|
+
self._token_decimals_cache[token_key] = decimals
|
|
68
|
+
logging.info(f'Token {token_address} uses {decimals} decimals')
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logging.warning(f'Failed to get decimals for {token_address}, defaulting to 18: {e}')
|
|
71
|
+
# Default to 18 if call fails (standard for most tokens)
|
|
72
|
+
decimals = 18
|
|
73
|
+
self._token_decimals_cache[token_key] = decimals
|
|
74
|
+
|
|
75
|
+
return self._token_decimals_cache[token_key]
|
|
76
|
+
|
|
77
|
+
def check_gas_balance(self, estimated_gas: int = 500000) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Check if signer has enough gas tokens (ETH) to execute transaction.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
estimated_gas: Estimated gas units needed (default: 500000)
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
InsufficientGasBalance: If signer doesn't have enough ETH for gas
|
|
86
|
+
"""
|
|
87
|
+
signer_address = self.signer.address()
|
|
88
|
+
gas_balance = self.w3.eth.get_balance(signer_address)
|
|
89
|
+
|
|
90
|
+
# Get current gas price with safety margin
|
|
91
|
+
base_fee = self.w3.eth.get_block('latest').get('baseFeePerGas', 0)
|
|
92
|
+
|
|
93
|
+
# For EIP-1559 chains, calculate max fee
|
|
94
|
+
if base_fee > 0:
|
|
95
|
+
# Priority fee (tip) - typically 1-2 gwei on Base
|
|
96
|
+
max_priority_fee = self.w3.to_wei(2, 'gwei')
|
|
97
|
+
# Max fee = base fee * 2 + priority fee (allows for 2x base fee increase)
|
|
98
|
+
max_fee_per_gas = (base_fee * 2) + max_priority_fee
|
|
99
|
+
gas_price = max_fee_per_gas
|
|
100
|
+
else:
|
|
101
|
+
# Fallback for legacy transactions
|
|
102
|
+
gas_price = self.w3.eth.gas_price
|
|
103
|
+
|
|
104
|
+
# Add 20% safety margin to estimated gas
|
|
105
|
+
estimated_gas_with_margin = int(estimated_gas * 1.2)
|
|
106
|
+
|
|
107
|
+
# Calculate required ETH (gas * gas_price)
|
|
108
|
+
required_eth = estimated_gas_with_margin * gas_price
|
|
109
|
+
|
|
110
|
+
if gas_balance < required_eth:
|
|
111
|
+
gas_balance_eth = self.w3.from_wei(gas_balance, 'ether')
|
|
112
|
+
required_eth_formatted = self.w3.from_wei(required_eth, 'ether')
|
|
113
|
+
gas_price_gwei = self.w3.from_wei(gas_price, 'gwei')
|
|
114
|
+
raise InsufficientGasBalance(
|
|
115
|
+
f"Insufficient gas balance. Signer {signer_address} has {gas_balance_eth} ETH, "
|
|
116
|
+
f"but needs approximately {required_eth_formatted} ETH for gas "
|
|
117
|
+
f"(gas: {estimated_gas_with_margin}, price: {gas_price_gwei} gwei)"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
logging.info(
|
|
121
|
+
f"Gas balance check passed. Signer has {self.w3.from_wei(gas_balance, 'ether')} ETH, "
|
|
122
|
+
f"estimated cost: {self.w3.from_wei(required_eth, 'ether')} ETH "
|
|
123
|
+
f"(gas: {estimated_gas_with_margin}, price: {self.w3.from_wei(gas_price, 'gwei')} gwei)"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def estimate_transaction_gas(self, tx_params: dict) -> int:
|
|
127
|
+
"""
|
|
128
|
+
Estimate gas for a transaction using web3's gas estimation.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tx_params: Transaction parameters dict with 'from', 'to', 'data', etc.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Estimated gas units needed
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
estimated = self.w3.eth.estimate_gas(tx_params)
|
|
138
|
+
logging.debug(f"Estimated gas for transaction: {estimated}")
|
|
139
|
+
return estimated
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logging.warning(f"Gas estimation failed, using fallback: {e}")
|
|
142
|
+
# Fallback to conservative estimate
|
|
143
|
+
return 500000
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def split(self, collateral_token: ChecksumAddress, condition_id: Hash32,
|
|
147
|
+
amount: int, partition: list = [1, 2], parent_collection_id: Hash32 = NULL_HASH) -> tuple[HexBytes, HexBytes, Any]:
|
|
148
|
+
|
|
149
|
+
# Check gas balance before executing transaction
|
|
150
|
+
self.check_gas_balance(estimated_gas=300000)
|
|
151
|
+
|
|
152
|
+
# Check balance of collateral
|
|
153
|
+
balance = self.get_erc20_contract(collateral_token).functions \
|
|
154
|
+
.balanceOf(self.multi_sig_addr).call()
|
|
155
|
+
logging.info(f'Collateral balance: {balance}')
|
|
156
|
+
if balance < amount:
|
|
157
|
+
raise BalanceNotEnough()
|
|
158
|
+
|
|
159
|
+
multi_send_txs: List[MultiSendTx] = []
|
|
160
|
+
|
|
161
|
+
data = HexBytes(
|
|
162
|
+
self.conditional_tokens.functions.splitPosition(
|
|
163
|
+
collateral_token, parent_collection_id, condition_id, partition, amount
|
|
164
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
multi_send_txs.append(MultiSendTx(
|
|
168
|
+
operation=MultiSendOperation.CALL.value,
|
|
169
|
+
to=self.conditional_tokens_addr,
|
|
170
|
+
value=0,
|
|
171
|
+
data=data,
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
tx_hash, safe_tx_hash, return_value = self.safe.execute_multisend(multi_send_txs)
|
|
175
|
+
|
|
176
|
+
# Validate transaction was successful
|
|
177
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
178
|
+
if receipt['status'] != 1:
|
|
179
|
+
raise Exception(f"Split transaction failed. Transaction hash: {tx_hash.hex()}")
|
|
180
|
+
|
|
181
|
+
logging.info(f"Split successful. Transaction hash: {tx_hash.hex()}")
|
|
182
|
+
return tx_hash, safe_tx_hash, return_value
|
|
183
|
+
|
|
184
|
+
def merge(self, collateral_token: ChecksumAddress, condition_id: Hash32,
|
|
185
|
+
amount: int, partition: list = [1, 2], parent_collection_id: Hash32 = NULL_HASH) -> tuple[HexBytes, HexBytes, Any]:
|
|
186
|
+
|
|
187
|
+
# Check gas balance before executing transaction
|
|
188
|
+
self.check_gas_balance(estimated_gas=300000)
|
|
189
|
+
|
|
190
|
+
# Check balance of positions
|
|
191
|
+
for index_set in partition:
|
|
192
|
+
position_id = self.get_position_id(condition_id, index_set=index_set, collateral_token=collateral_token)
|
|
193
|
+
balance = self.conditional_tokens.functions \
|
|
194
|
+
.balanceOf(self.multi_sig_addr, position_id).call()
|
|
195
|
+
# print('balance: {}'.format(balance))
|
|
196
|
+
if balance < amount:
|
|
197
|
+
raise BalanceNotEnough()
|
|
198
|
+
|
|
199
|
+
multi_send_txs: List[MultiSendTx] = []
|
|
200
|
+
|
|
201
|
+
data = HexBytes(
|
|
202
|
+
self.conditional_tokens.functions.mergePositions(
|
|
203
|
+
collateral_token, parent_collection_id, condition_id, partition, amount
|
|
204
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
multi_send_txs.append(MultiSendTx(
|
|
208
|
+
operation=MultiSendOperation.CALL.value,
|
|
209
|
+
to=self.conditional_tokens_addr,
|
|
210
|
+
value=0,
|
|
211
|
+
data=data,
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
tx_hash, safe_tx_hash, return_value = self.safe.execute_multisend(multi_send_txs)
|
|
215
|
+
|
|
216
|
+
# Validate transaction was successful
|
|
217
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
218
|
+
if receipt['status'] != 1:
|
|
219
|
+
raise Exception(f"Merge transaction failed. Transaction hash: {tx_hash.hex()}")
|
|
220
|
+
|
|
221
|
+
logging.info(f"Merge successful. Transaction hash: {tx_hash.hex()}")
|
|
222
|
+
return tx_hash, safe_tx_hash, return_value
|
|
223
|
+
|
|
224
|
+
def redeem(self, collateral_token: ChecksumAddress, condition_id: Hash32,
|
|
225
|
+
partition: list = [1, 2], parent_collection_id: Hash32 = NULL_HASH) -> tuple[HexBytes, HexBytes, Any]:
|
|
226
|
+
|
|
227
|
+
# Check gas balance before executing transaction
|
|
228
|
+
self.check_gas_balance(estimated_gas=300000)
|
|
229
|
+
|
|
230
|
+
# Check balance of positions
|
|
231
|
+
has_positions = False
|
|
232
|
+
for index_set in partition:
|
|
233
|
+
position_id = self.get_position_id(condition_id, index_set=index_set, collateral_token=collateral_token)
|
|
234
|
+
balance = self.conditional_tokens.functions \
|
|
235
|
+
.balanceOf(self.multi_sig_addr, position_id).call()
|
|
236
|
+
# print('balance: {}'.format(balance))
|
|
237
|
+
if balance > 0:
|
|
238
|
+
has_positions = True
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
if not has_positions:
|
|
242
|
+
raise NoPositionsToRedeem
|
|
243
|
+
|
|
244
|
+
multi_send_txs: List[MultiSendTx] = []
|
|
245
|
+
|
|
246
|
+
data = HexBytes(
|
|
247
|
+
self.conditional_tokens.functions.redeemPositions(
|
|
248
|
+
collateral_token, parent_collection_id, condition_id, partition
|
|
249
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
multi_send_txs.append(MultiSendTx(
|
|
253
|
+
operation=MultiSendOperation.CALL.value,
|
|
254
|
+
to=self.conditional_tokens_addr,
|
|
255
|
+
value=0,
|
|
256
|
+
data=data,
|
|
257
|
+
))
|
|
258
|
+
|
|
259
|
+
tx_hash, safe_tx_hash, return_value = self.safe.execute_multisend(multi_send_txs)
|
|
260
|
+
|
|
261
|
+
# Validate transaction was successful
|
|
262
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
263
|
+
if receipt['status'] != 1:
|
|
264
|
+
raise Exception(f"Redeem transaction failed. Transaction hash: {tx_hash.hex()}")
|
|
265
|
+
|
|
266
|
+
logging.info(f"Redeem successful. Transaction hash: {tx_hash.hex()}")
|
|
267
|
+
return tx_hash, safe_tx_hash, return_value
|
|
268
|
+
|
|
269
|
+
def enable_trading(self, supported_quote_tokens: dict) -> tuple[HexBytes, HexBytes, Any]:
|
|
270
|
+
if self.__enable_trading_last_time is not None and \
|
|
271
|
+
time.time() - self.__enable_trading_last_time < self.__enable_trading_check_interval:
|
|
272
|
+
return HexBytes(b'0x'), HexBytes(b'0x'), None
|
|
273
|
+
|
|
274
|
+
self.__enable_trading_last_time = time.time()
|
|
275
|
+
|
|
276
|
+
# Check gas balance before executing transaction (approve operations can be gas-heavy)
|
|
277
|
+
self.check_gas_balance(estimated_gas=500000)
|
|
278
|
+
|
|
279
|
+
multi_send_txs: List[MultiSendTx] = []
|
|
280
|
+
|
|
281
|
+
from .contracts.erc20 import abi
|
|
282
|
+
for erc20_address, ctf_exchange_address in supported_quote_tokens.items():
|
|
283
|
+
erc20_contract = self.w3.eth.contract(erc20_address, abi=abi)
|
|
284
|
+
allowance = erc20_contract.functions.allowance(self.multi_sig_addr, ctf_exchange_address).call()
|
|
285
|
+
|
|
286
|
+
# Get actual token decimals from contract
|
|
287
|
+
decimals = self.get_token_decimals(erc20_address)
|
|
288
|
+
|
|
289
|
+
# Used for trading on ctf_exchange
|
|
290
|
+
min_threshold = 1000000000 * 10**decimals
|
|
291
|
+
allowance_to_update = 2*1000000000 * 10**decimals
|
|
292
|
+
if allowance < min_threshold:
|
|
293
|
+
# DH1 Fix: Reset approval to 0 first (required for some tokens like USDT)
|
|
294
|
+
# to prevent approval race condition attack
|
|
295
|
+
if allowance > 0:
|
|
296
|
+
reset_data = HexBytes(
|
|
297
|
+
erc20_contract.functions.approve(
|
|
298
|
+
ctf_exchange_address, 0
|
|
299
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
300
|
+
)
|
|
301
|
+
multi_send_txs.append(MultiSendTx(
|
|
302
|
+
operation=MultiSendOperation.CALL.value,
|
|
303
|
+
to=erc20_address,
|
|
304
|
+
value=0,
|
|
305
|
+
data=reset_data,
|
|
306
|
+
))
|
|
307
|
+
logging.info(f'Resetting approval to 0 for {erc20_address} -> {ctf_exchange_address}')
|
|
308
|
+
|
|
309
|
+
# Now set the new approval amount
|
|
310
|
+
data = HexBytes(
|
|
311
|
+
erc20_contract.functions.approve(
|
|
312
|
+
ctf_exchange_address, allowance_to_update
|
|
313
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
multi_send_txs.append(MultiSendTx(
|
|
317
|
+
operation=MultiSendOperation.CALL.value,
|
|
318
|
+
to=erc20_address,
|
|
319
|
+
value=0,
|
|
320
|
+
data=data,
|
|
321
|
+
))
|
|
322
|
+
|
|
323
|
+
# Used for splitting
|
|
324
|
+
allowance = erc20_contract.functions.allowance(self.multi_sig_addr, self.conditional_tokens_addr).call()
|
|
325
|
+
if allowance < min_threshold:
|
|
326
|
+
# DH1 Fix: Reset approval to 0 first (required for some tokens like USDT)
|
|
327
|
+
if allowance > 0:
|
|
328
|
+
reset_data = HexBytes(
|
|
329
|
+
erc20_contract.functions.approve(
|
|
330
|
+
self.conditional_tokens_addr, 0
|
|
331
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
332
|
+
)
|
|
333
|
+
multi_send_txs.append(MultiSendTx(
|
|
334
|
+
operation=MultiSendOperation.CALL.value,
|
|
335
|
+
to=erc20_address,
|
|
336
|
+
value=0,
|
|
337
|
+
data=reset_data,
|
|
338
|
+
))
|
|
339
|
+
logging.info(f'Resetting approval to 0 for {erc20_address} -> {self.conditional_tokens_addr}')
|
|
340
|
+
|
|
341
|
+
# Now set the new approval amount
|
|
342
|
+
data = HexBytes(
|
|
343
|
+
erc20_contract.functions.approve(
|
|
344
|
+
self.conditional_tokens_addr, allowance_to_update
|
|
345
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
multi_send_txs.append(MultiSendTx(
|
|
349
|
+
operation=MultiSendOperation.CALL.value,
|
|
350
|
+
to=erc20_address,
|
|
351
|
+
value=0,
|
|
352
|
+
data=data,
|
|
353
|
+
))
|
|
354
|
+
|
|
355
|
+
# Approve ctf_exchange for using conditional tokens
|
|
356
|
+
is_approved_for_all = self.conditional_tokens.functions.isApprovedForAll(
|
|
357
|
+
self.multi_sig_addr, ctf_exchange_address).call()
|
|
358
|
+
if is_approved_for_all is False:
|
|
359
|
+
data = HexBytes(
|
|
360
|
+
self.conditional_tokens.functions.setApprovalForAll(
|
|
361
|
+
ctf_exchange_address, True
|
|
362
|
+
).build_transaction(get_empty_tx_params())["data"]
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
multi_send_txs.append(MultiSendTx(
|
|
366
|
+
operation=MultiSendOperation.CALL.value,
|
|
367
|
+
to=self.conditional_tokens_addr,
|
|
368
|
+
value=0,
|
|
369
|
+
data=data,
|
|
370
|
+
))
|
|
371
|
+
|
|
372
|
+
if len(multi_send_txs) > 0:
|
|
373
|
+
tx_hash, safe_tx_hash, return_value = self.safe.execute_multisend(multi_send_txs)
|
|
374
|
+
|
|
375
|
+
# Validate transaction was successful
|
|
376
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
377
|
+
if receipt['status'] != 1:
|
|
378
|
+
raise Exception(f"Enable trading transaction failed. Transaction hash: {tx_hash.hex()}")
|
|
379
|
+
|
|
380
|
+
logging.info(f"Enable trading successful. Transaction hash: {tx_hash.hex()}")
|
|
381
|
+
return tx_hash, safe_tx_hash, return_value
|
|
382
|
+
else:
|
|
383
|
+
return HexBytes(b'0x'), HexBytes(b'0x'), None
|
|
384
|
+
|
|
385
|
+
def get_position_id(self, condition_id: Hash32, index_set: int, collateral_token: ChecksumAddress,
|
|
386
|
+
parent_condition_id=NULL_HASH):
|
|
387
|
+
collection_id = self.conditional_tokens.functions.getCollectionId(
|
|
388
|
+
parent_condition_id, condition_id, index_set).call()
|
|
389
|
+
|
|
390
|
+
return self.conditional_tokens.functions.getPositionId(collateral_token, collection_id).call()
|
|
File without changes
|