opinion-clob-sdk 0.1.9__py3-none-any.whl → 0.1.10__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/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +52 -28
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +139 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/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/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/sdk.py +957 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/verify_api_calls.py +135 -0
- {opinion_clob_sdk-0.1.9.dist-info → opinion_clob_sdk-0.1.10.dist-info}/METADATA +1 -1
- {opinion_clob_sdk-0.1.9.dist-info → opinion_clob_sdk-0.1.10.dist-info}/RECORD +65 -22
- {opinion_clob_sdk-0.1.9.dist-info → opinion_clob_sdk-0.1.10.dist-info}/WHEEL +0 -0
- {opinion_clob_sdk-0.1.9.dist-info → opinion_clob_sdk-0.1.10.dist-info}/top_level.txt +0 -0
|
@@ -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()
|