opinion-clob-sdk 0.1.3__py3-none-any.whl → 0.1.4__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.

Files changed (54) hide show
  1. opinion_clob_sdk/__init__.py +1 -1
  2. opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
  3. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
  4. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
  5. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
  6. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
  7. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
  8. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/conditional_tokens.py +707 -0
  9. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
  10. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
  11. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/__init__.py +0 -0
  12. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/__init__.py +0 -0
  13. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/base_builder.py +41 -0
  14. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/exception.py +2 -0
  15. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder.py +90 -0
  16. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder_test.py +40 -0
  17. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/constants.py +2 -0
  18. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/__init__.py +0 -0
  19. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order.py +254 -0
  20. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order_type.py +9 -0
  21. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/sides.py +8 -0
  22. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/signatures.py +8 -0
  23. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/signer.py +20 -0
  24. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +109 -0
  25. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
  26. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
  27. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/eip712/__init__.py +176 -0
  28. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
  29. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
  30. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
  31. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
  32. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/__init__.py +0 -0
  33. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/compatibility_fallback_handler_v1_3_0.py +327 -0
  34. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/multisend_v1_3_0.py +22 -0
  35. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/safe_v1_3_0.py +1035 -0
  36. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/utils.py +26 -0
  37. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_signature.py +364 -0
  38. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_test.py +37 -0
  39. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_tx.py +437 -0
  40. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
  41. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
  42. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
  43. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
  44. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
  45. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/sdk.py +947 -0
  46. opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/sdk.py +9 -9
  47. opinion_clob_sdk/opinion_clob_sdk/sdk.py +9 -9
  48. opinion_clob_sdk/opinion_clob_sdk/verify_api_calls.py +135 -0
  49. opinion_clob_sdk/sdk.py +9 -9
  50. opinion_clob_sdk/verify_api_calls.py +135 -0
  51. {opinion_clob_sdk-0.1.3.dist-info → opinion_clob_sdk-0.1.4.dist-info}/METADATA +1 -1
  52. {opinion_clob_sdk-0.1.3.dist-info → opinion_clob_sdk-0.1.4.dist-info}/RECORD +54 -10
  53. {opinion_clob_sdk-0.1.3.dist-info → opinion_clob_sdk-0.1.4.dist-info}/WHEEL +0 -0
  54. {opinion_clob_sdk-0.1.3.dist-info → opinion_clob_sdk-0.1.4.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ from opinion_clob_sdk.chain.exception import (
12
12
  InsufficientGasBalance
13
13
  )
14
14
 
15
- __version__ = "0.1.3"
15
+ __version__ = "0.1.4"
16
16
  __all__ = [
17
17
  "Client",
18
18
  "TopicStatus",
@@ -12,7 +12,7 @@ from opinion_clob_sdk.chain.exception import (
12
12
  InsufficientGasBalance
13
13
  )
14
14
 
15
- __version__ = "0.1.3"
15
+ __version__ = "0.1.4"
16
16
  __all__ = [
17
17
  "Client",
18
18
  "TopicStatus",
@@ -12,7 +12,7 @@ from opinion_clob_sdk.chain.exception import (
12
12
  InsufficientGasBalance
13
13
  )
14
14
 
15
- __version__ = "0.1.3"
15
+ __version__ = "0.1.4"
16
16
  __all__ = [
17
17
  "Client",
18
18
  "TopicStatus",
@@ -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.4"
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
+ ]
@@ -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()