iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Contract interaction helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import warnings
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from eth_abi import decode
|
|
9
|
+
from web3 import Web3
|
|
10
|
+
from web3.contract import Contract
|
|
11
|
+
from web3.exceptions import ContractCustomError
|
|
12
|
+
|
|
13
|
+
from iwa.core.chain import ChainInterfaces
|
|
14
|
+
from iwa.core.utils import configure_logger
|
|
15
|
+
|
|
16
|
+
logger = configure_logger()
|
|
17
|
+
|
|
18
|
+
# Standard error selectors
|
|
19
|
+
ERROR_SELECTOR = "0x08c379a0" # Error(string)
|
|
20
|
+
PANIC_SELECTOR = "0x4e487b71" # Panic(uint256)
|
|
21
|
+
|
|
22
|
+
# Panic codes (from Solidity)
|
|
23
|
+
PANIC_CODES = {
|
|
24
|
+
0x00: "Generic compiler inserted panic",
|
|
25
|
+
0x01: "Assert failed",
|
|
26
|
+
0x11: "Arithmetic overflow/underflow",
|
|
27
|
+
0x12: "Division by zero",
|
|
28
|
+
0x21: "Invalid enum value",
|
|
29
|
+
0x22: "Storage byte array incorrectly encoded",
|
|
30
|
+
0x31: "Pop on empty array",
|
|
31
|
+
0x32: "Array index out of bounds",
|
|
32
|
+
0x41: "Too much memory allocated",
|
|
33
|
+
0x51: "Invalid internal function call",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContractInstance:
|
|
38
|
+
"""Class to interact with smart contracts."""
|
|
39
|
+
|
|
40
|
+
name: str = None
|
|
41
|
+
abi_path: Path = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, address: str, chain_name: str = "gnosis"):
|
|
44
|
+
"""Initialize contract instance."""
|
|
45
|
+
self.address = address
|
|
46
|
+
self.abi = None
|
|
47
|
+
self.chain_interface = ChainInterfaces().get(chain_name)
|
|
48
|
+
|
|
49
|
+
with open(self.abi_path, "r", encoding="utf-8") as abi_file:
|
|
50
|
+
contract_abi = json.load(abi_file)
|
|
51
|
+
|
|
52
|
+
if isinstance(contract_abi, dict) and "abi" in contract_abi:
|
|
53
|
+
self.abi = contract_abi.get("abi")
|
|
54
|
+
else:
|
|
55
|
+
self.abi = contract_abi
|
|
56
|
+
|
|
57
|
+
self.contract: Contract = self.chain_interface.web3.eth.contract(
|
|
58
|
+
address=self.address, abi=self.abi
|
|
59
|
+
)
|
|
60
|
+
self.error_selectors = self.load_error_selectors()
|
|
61
|
+
|
|
62
|
+
def load_error_selectors(self) -> Dict[str, Any]:
|
|
63
|
+
"""Load error selectors from the contract ABI."""
|
|
64
|
+
selectors = {}
|
|
65
|
+
for entry in self.abi:
|
|
66
|
+
if entry.get("type") == "error":
|
|
67
|
+
name = entry["name"]
|
|
68
|
+
inputs = entry.get("inputs", [])
|
|
69
|
+
types = ",".join(i["type"] for i in inputs)
|
|
70
|
+
signature = f"{name}({types})"
|
|
71
|
+
selector = Web3.keccak(text=signature)[:4].hex()
|
|
72
|
+
selectors[f"0x{selector}"] = (
|
|
73
|
+
name,
|
|
74
|
+
[i["type"] for i in inputs],
|
|
75
|
+
[i["name"] for i in inputs],
|
|
76
|
+
)
|
|
77
|
+
return selectors
|
|
78
|
+
|
|
79
|
+
def decode_error(self, error_data: str) -> Optional[Tuple[str, str]]:
|
|
80
|
+
"""Decode error data from a failed transaction or call.
|
|
81
|
+
|
|
82
|
+
Handles:
|
|
83
|
+
- Custom errors defined in the contract ABI
|
|
84
|
+
- Standard Error(string) reverts
|
|
85
|
+
- Panic(uint256) errors
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
error_data: The hex-encoded error data (with or without 0x prefix).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (error_name, formatted_message) or None if decoding fails.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
if not error_data:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Normalize data
|
|
98
|
+
if not error_data.startswith("0x"):
|
|
99
|
+
error_data = f"0x{error_data}"
|
|
100
|
+
|
|
101
|
+
if len(error_data) < 10:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
selector = error_data[:10]
|
|
105
|
+
encoded_args = error_data[10:]
|
|
106
|
+
|
|
107
|
+
# Check for custom errors from ABI
|
|
108
|
+
if selector in self.error_selectors:
|
|
109
|
+
error_name, types, names = self.error_selectors[selector]
|
|
110
|
+
try:
|
|
111
|
+
decoded = decode(types, bytes.fromhex(encoded_args))
|
|
112
|
+
error_str = ", ".join(
|
|
113
|
+
f"{name}={value}" for name, value in zip(names, decoded, strict=True)
|
|
114
|
+
)
|
|
115
|
+
return (error_name, f"{error_name}({error_str})")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.debug(f"Failed to decode custom error args: {e}")
|
|
118
|
+
return (error_name, f"{error_name}(decoding failed)")
|
|
119
|
+
|
|
120
|
+
# Check for standard Error(string)
|
|
121
|
+
if selector == ERROR_SELECTOR:
|
|
122
|
+
try:
|
|
123
|
+
decoded = decode(["string"], bytes.fromhex(encoded_args))
|
|
124
|
+
return ("Error", decoded[0])
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.debug(f"Failed to decode Error(string): {e}")
|
|
127
|
+
return ("Error", "Failed to decode error message")
|
|
128
|
+
|
|
129
|
+
# Check for Panic(uint256)
|
|
130
|
+
if selector == PANIC_SELECTOR:
|
|
131
|
+
try:
|
|
132
|
+
decoded = decode(["uint256"], bytes.fromhex(encoded_args))
|
|
133
|
+
panic_code = decoded[0]
|
|
134
|
+
panic_msg = PANIC_CODES.get(panic_code, f"Unknown panic code: {panic_code}")
|
|
135
|
+
return ("Panic", panic_msg)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.debug(f"Failed to decode Panic(uint256): {e}")
|
|
138
|
+
return ("Panic", "Failed to decode panic code")
|
|
139
|
+
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def _extract_error_data(self, exception: Exception) -> Optional[str]:
|
|
143
|
+
"""Extract error data from various exception formats.
|
|
144
|
+
|
|
145
|
+
Different RPC providers and web3 versions format errors differently.
|
|
146
|
+
This method tries to extract the error data from common formats.
|
|
147
|
+
"""
|
|
148
|
+
# ContractCustomError has data directly
|
|
149
|
+
if isinstance(exception, ContractCustomError) and exception.args:
|
|
150
|
+
return exception.args[0] if isinstance(exception.args[0], str) else None
|
|
151
|
+
|
|
152
|
+
# Check exception args for hex data
|
|
153
|
+
args = getattr(exception, "args", ())
|
|
154
|
+
for arg in args:
|
|
155
|
+
if isinstance(arg, str) and arg.startswith("0x"):
|
|
156
|
+
return arg
|
|
157
|
+
if isinstance(arg, dict):
|
|
158
|
+
# Some providers return {"data": "0x..."}
|
|
159
|
+
data = arg.get("data")
|
|
160
|
+
if isinstance(data, str) and data.startswith("0x"):
|
|
161
|
+
return data
|
|
162
|
+
|
|
163
|
+
# Check for 'data' attribute
|
|
164
|
+
data = getattr(exception, "data", None)
|
|
165
|
+
if isinstance(data, str) and data.startswith("0x"):
|
|
166
|
+
return data
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def call(self, method_name: str, *args) -> Any:
|
|
171
|
+
"""Call a function in the contract without sending a transaction.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
method_name: The name of the contract function to call.
|
|
175
|
+
*args: Arguments to pass to the function.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The return value of the contract function.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
Exception: If the call fails, with decoded error information.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
method = getattr(self.contract.functions, method_name)
|
|
185
|
+
try:
|
|
186
|
+
return method(*args).call()
|
|
187
|
+
except Exception as e:
|
|
188
|
+
error_data = self._extract_error_data(e)
|
|
189
|
+
if error_data:
|
|
190
|
+
decoded = self.decode_error(error_data)
|
|
191
|
+
if decoded:
|
|
192
|
+
error_name, error_msg = decoded
|
|
193
|
+
logger.error(
|
|
194
|
+
f"Contract call '{method_name}' on {self.name}[{self.address}] "
|
|
195
|
+
f"failed: {error_name}: {error_msg}"
|
|
196
|
+
)
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
def _sanitize_for_web3(self, value: Any) -> Any:
|
|
200
|
+
"""Convert EthereumAddress subclass to pure str for eth_abi encoding.
|
|
201
|
+
|
|
202
|
+
eth_abi encoder cannot handle custom str subclasses; this ensures
|
|
203
|
+
all address strings are pure str instances.
|
|
204
|
+
"""
|
|
205
|
+
if isinstance(value, str) and type(value) is not str:
|
|
206
|
+
# It's a str subclass (like EthereumAddress), convert to pure str
|
|
207
|
+
return str.__str__(value)
|
|
208
|
+
if isinstance(value, dict):
|
|
209
|
+
return {k: self._sanitize_for_web3(v) for k, v in value.items()}
|
|
210
|
+
if isinstance(value, (list, tuple)):
|
|
211
|
+
return type(value)(self._sanitize_for_web3(v) for v in value)
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
def prepare_transaction(
|
|
215
|
+
self, method_name: str, method_kwargs: Dict, tx_params: Dict
|
|
216
|
+
) -> Optional[dict]:
|
|
217
|
+
"""Prepare a transaction.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
method_name: The name of the contract function to call.
|
|
221
|
+
method_kwargs: Dictionary of keyword arguments for the function.
|
|
222
|
+
tx_params: Transaction parameters (from, gas, etc.).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The prepared transaction dict, or None if preparation failed.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
# Sanitize kwargs and params to convert EthereumAddress to pure str
|
|
229
|
+
method_kwargs = self._sanitize_for_web3(method_kwargs)
|
|
230
|
+
tx_params = self._sanitize_for_web3(tx_params)
|
|
231
|
+
|
|
232
|
+
method = getattr(self.contract.functions, method_name)
|
|
233
|
+
built_method = method(*method_kwargs.values())
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
tx_params = self.chain_interface.calculate_transaction_params(built_method, tx_params)
|
|
237
|
+
transaction = built_method.build_transaction(tx_params)
|
|
238
|
+
return transaction
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error_data = self._extract_error_data(e)
|
|
242
|
+
if error_data:
|
|
243
|
+
decoded = self.decode_error(error_data)
|
|
244
|
+
if decoded:
|
|
245
|
+
error_name, error_msg = decoded
|
|
246
|
+
logger.error(
|
|
247
|
+
f"Failed to prepare '{method_name}' on {self.name}[{self.address}]: "
|
|
248
|
+
f"{error_name}: {error_msg}"
|
|
249
|
+
)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
# Fallback: log the raw exception
|
|
253
|
+
logger.error(f"Failed to prepare '{method_name}': {e}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def extract_events(self, receipt) -> List[Dict]:
|
|
257
|
+
"""Extract events from a transaction receipt.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
receipt: The transaction receipt.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of event dictionaries with 'name' and 'args' keys.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
all_events = []
|
|
267
|
+
|
|
268
|
+
if not receipt:
|
|
269
|
+
return all_events
|
|
270
|
+
|
|
271
|
+
for event_abi in self.contract.abi:
|
|
272
|
+
# Skip non events
|
|
273
|
+
if event_abi.get("type") != "event":
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
event_name = event_abi.get("name", "Unknown")
|
|
277
|
+
try:
|
|
278
|
+
event = self.contract.events[event_name]
|
|
279
|
+
except KeyError:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
with warnings.catch_warnings():
|
|
283
|
+
warnings.simplefilter("ignore")
|
|
284
|
+
try:
|
|
285
|
+
decoded_logs = event().process_receipt(receipt)
|
|
286
|
+
|
|
287
|
+
if not decoded_logs:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
for log in decoded_logs:
|
|
291
|
+
all_events.append({"name": log["event"], "args": dict(log.args)})
|
|
292
|
+
except Exception as e:
|
|
293
|
+
# Log at debug level to avoid noise, but capture the issue
|
|
294
|
+
logger.debug(f"Failed to decode event '{event_name}' from {self.name}: {e}")
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
return all_events
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""ERC20 contract interaction."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
from iwa.core.constants import ABI_PATH
|
|
6
|
+
from iwa.core.contracts.contract import ContractInstance
|
|
7
|
+
from iwa.core.types import EthereumAddress
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ERC20Contract(ContractInstance):
|
|
11
|
+
"""Class to interact with ERC20 contracts."""
|
|
12
|
+
|
|
13
|
+
name = "erc20"
|
|
14
|
+
abi_path = ABI_PATH / "erc20.json"
|
|
15
|
+
|
|
16
|
+
def __init__(self, address: EthereumAddress, chain_name: str = "gnosis"):
|
|
17
|
+
"""Initialize ERC20 contract instance."""
|
|
18
|
+
super().__init__(address, chain_name)
|
|
19
|
+
|
|
20
|
+
self.decimals = self.call("decimals")
|
|
21
|
+
self.symbol = self.call("symbol")
|
|
22
|
+
self.name = self.call("name")
|
|
23
|
+
self.total_supply = self.call("totalSupply")
|
|
24
|
+
|
|
25
|
+
def allowance_wei(self, owner: EthereumAddress, spender: EthereumAddress) -> int:
|
|
26
|
+
"""Allowance"""
|
|
27
|
+
return self.call("allowance", owner, spender)
|
|
28
|
+
|
|
29
|
+
def allowance_eth(self, owner: EthereumAddress, spender: EthereumAddress) -> float:
|
|
30
|
+
"""Allowance in human readable format"""
|
|
31
|
+
return self.allowance_wei(owner, spender) / (10**self.decimals)
|
|
32
|
+
|
|
33
|
+
def balance_of_wei(self, account: EthereumAddress) -> int:
|
|
34
|
+
"""Balance of"""
|
|
35
|
+
return self.call("balanceOf", account)
|
|
36
|
+
|
|
37
|
+
def balance_of_eth(self, account: EthereumAddress) -> float:
|
|
38
|
+
"""Balance of in human readable format"""
|
|
39
|
+
return self.balance_of_wei(account) / (10**self.decimals)
|
|
40
|
+
|
|
41
|
+
def prepare_transfer_tx(
|
|
42
|
+
self,
|
|
43
|
+
from_address: EthereumAddress,
|
|
44
|
+
to: EthereumAddress,
|
|
45
|
+
amount_wei: int,
|
|
46
|
+
) -> Optional[Dict]:
|
|
47
|
+
"""Transfer."""
|
|
48
|
+
return self.prepare_transaction(
|
|
49
|
+
method_name="transfer",
|
|
50
|
+
method_kwargs={"to": to, "amount": amount_wei},
|
|
51
|
+
tx_params={"from": from_address},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def prepare_transfer_from_tx(
|
|
55
|
+
self,
|
|
56
|
+
from_address: EthereumAddress,
|
|
57
|
+
sender: EthereumAddress,
|
|
58
|
+
recipient: EthereumAddress,
|
|
59
|
+
amount_wei: int,
|
|
60
|
+
) -> Optional[Dict]:
|
|
61
|
+
"""Transfer from."""
|
|
62
|
+
return self.prepare_transaction(
|
|
63
|
+
method_name="transferFrom",
|
|
64
|
+
method_kwargs={"_sender": sender, "_recipient": recipient, "_amount": amount_wei},
|
|
65
|
+
tx_params={"from": from_address},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def prepare_approve_tx(
|
|
69
|
+
self,
|
|
70
|
+
from_address: EthereumAddress,
|
|
71
|
+
spender: EthereumAddress,
|
|
72
|
+
amount_wei: int,
|
|
73
|
+
) -> Optional[Dict]:
|
|
74
|
+
"""Approve."""
|
|
75
|
+
return self.prepare_transaction(
|
|
76
|
+
method_name="approve",
|
|
77
|
+
method_kwargs={"spender": spender, "amount": amount_wei},
|
|
78
|
+
tx_params={"from": from_address},
|
|
79
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Multisend contract interaction."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, cast
|
|
4
|
+
|
|
5
|
+
from hexbytes import HexBytes
|
|
6
|
+
from safe_eth.safe import SafeOperationEnum
|
|
7
|
+
|
|
8
|
+
from iwa.core.constants import ABI_PATH
|
|
9
|
+
from iwa.core.contracts.contract import ContractInstance
|
|
10
|
+
from iwa.core.types import EthereumAddress
|
|
11
|
+
|
|
12
|
+
# MultiSend addresses (same across Ethereum, Base, Gnosis via Singleton Factory)
|
|
13
|
+
MULTISEND_CALL_ONLY_ADDRESS = EthereumAddress("0x40A2aCCbd92BCA938b02010E17A5b8929b49130D")
|
|
14
|
+
MULTISEND_ADDRESS = EthereumAddress("0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MultiSendCallOnlyContract(ContractInstance):
|
|
18
|
+
"""Class to interact with multisend (call only) contract."""
|
|
19
|
+
|
|
20
|
+
name = "multisend_call_only"
|
|
21
|
+
abi_path = ABI_PATH / "multisend_call_only.json"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def encode_data(tx: Dict) -> bytes:
|
|
25
|
+
"""Encodes multisend transaction."""
|
|
26
|
+
# Operation 1 byte
|
|
27
|
+
operation = HexBytes("{:0>2x}".format(cast(SafeOperationEnum, tx.get("operation")).value))
|
|
28
|
+
|
|
29
|
+
# Address 20 bytes
|
|
30
|
+
to = HexBytes("{:0>40x}".format(int(cast(str, tx.get("to")), 16)))
|
|
31
|
+
|
|
32
|
+
# Value 32 bytes
|
|
33
|
+
value = HexBytes("{:0>64x}".format(cast(int, tx.get("value", 0))))
|
|
34
|
+
|
|
35
|
+
# Data length 32 bytes
|
|
36
|
+
data = cast(bytes, tx.get("data", b""))
|
|
37
|
+
data_ = HexBytes(data)
|
|
38
|
+
data_length = HexBytes("{:0>64x}".format(len(data_)))
|
|
39
|
+
|
|
40
|
+
return operation + to + value + data_length + data_
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def to_bytes(multi_send_txs: List[Dict]) -> bytes:
|
|
44
|
+
"""Multi send tx list to bytes."""
|
|
45
|
+
return b"".join([MultiSendCallOnlyContract.encode_data(tx) for tx in multi_send_txs])
|
|
46
|
+
|
|
47
|
+
def prepare_tx(
|
|
48
|
+
self,
|
|
49
|
+
from_address: EthereumAddress,
|
|
50
|
+
transactions: list,
|
|
51
|
+
) -> Optional[Dict]:
|
|
52
|
+
"""Prepare multisend transaction."""
|
|
53
|
+
encoded_multisend_data = MultiSendCallOnlyContract.to_bytes(transactions)
|
|
54
|
+
|
|
55
|
+
total_value_wei = sum([tx.get("value", 0) for tx in transactions])
|
|
56
|
+
|
|
57
|
+
return self.prepare_transaction(
|
|
58
|
+
method_name="multiSend",
|
|
59
|
+
method_kwargs={"encoded_multisend_data": encoded_multisend_data},
|
|
60
|
+
tx_params={
|
|
61
|
+
"from": from_address,
|
|
62
|
+
"value": total_value_wei,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MultiSendContract(MultiSendCallOnlyContract):
|
|
68
|
+
"""Class to interact with multisend contract."""
|
|
69
|
+
|
|
70
|
+
name = "multisend"
|
|
71
|
+
abi_path = ABI_PATH / "multisend.json"
|