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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,336 @@
1
+ """Drain manager mixin."""
2
+
3
+ from typing import Any, Dict, Optional, Tuple
4
+
5
+ from loguru import logger
6
+
7
+ from iwa.core.contracts.erc20 import ERC20Contract
8
+ from iwa.plugins.olas.constants import OLAS_TOKEN_ADDRESS_GNOSIS
9
+ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
10
+
11
+
12
+ class DrainManagerMixin:
13
+ """Mixin for draining and service token management."""
14
+
15
+ def claim_rewards(self, staking_contract: Optional[StakingContract] = None) -> Tuple[bool, int]:
16
+ """Claim staking rewards for the active service.
17
+
18
+ The claimed OLAS tokens will be sent to the service's multisig (Safe).
19
+
20
+ Args:
21
+ staking_contract: Optional pre-loaded StakingContract. If not provided,
22
+ it will be loaded from the service's staking_contract_address.
23
+
24
+ Returns:
25
+ Tuple of (success, claimed_amount_wei).
26
+
27
+ """
28
+ if not self.service:
29
+ logger.error("No active service")
30
+ return False, 0
31
+
32
+ if not self.service.staking_contract_address:
33
+ logger.error("Service is not staked")
34
+ return False, 0
35
+
36
+ # Load staking contract if not provided
37
+ if not staking_contract:
38
+ try:
39
+ staking_contract = StakingContract(
40
+ str(self.service.staking_contract_address),
41
+ chain_name=self.chain_name,
42
+ )
43
+ except Exception as e:
44
+ logger.error(f"Failed to load staking contract: {e}")
45
+ return False, 0
46
+
47
+ service_id = self.service.service_id
48
+
49
+ # Check if actually staked
50
+ if staking_contract.get_staking_state(service_id) != StakingState.STAKED:
51
+ logger.info("Service not staked, skipping claim")
52
+ return False, 0
53
+
54
+ # Check accrued rewards
55
+ accrued_rewards = staking_contract.get_accrued_rewards(service_id)
56
+ if accrued_rewards == 0:
57
+ logger.info("No accrued rewards to claim")
58
+ return False, 0
59
+
60
+ logger.info(f"Claiming {accrued_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
61
+
62
+ # Prepare and send claim transaction
63
+ claim_tx = staking_contract.prepare_claim_tx(
64
+ from_address=self.wallet.master_account.address,
65
+ service_id=service_id,
66
+ )
67
+
68
+ if not claim_tx:
69
+ logger.error("Failed to prepare claim transaction")
70
+ return False, 0
71
+
72
+ success, receipt = self.wallet.sign_and_send_transaction(
73
+ claim_tx,
74
+ signer_address_or_tag=self.wallet.master_account.address,
75
+ chain_name=self.chain_name,
76
+ tags=["olas_claim_rewards"],
77
+ )
78
+ if not success:
79
+ logger.error("Failed to send claim transaction")
80
+ return False, 0
81
+
82
+ events = staking_contract.extract_events(receipt)
83
+ if "RewardClaimed" not in [event["name"] for event in events]:
84
+ logger.warning("RewardClaimed event not found, but transaction succeeded")
85
+
86
+ logger.info(f"Successfully claimed {accrued_rewards / 1e18:.4f} OLAS rewards")
87
+ return True, accrued_rewards
88
+
89
+ def withdraw_rewards(self) -> Tuple[bool, float]:
90
+ """Withdraw OLAS from the service Safe to the configured withdrawal address.
91
+
92
+ The OLAS tokens are transferred from the service's multisig to the
93
+ withdrawal_address configured in the OlasConfig.
94
+
95
+ Returns:
96
+ Tuple of (success, olas_amount_transferred).
97
+
98
+ """
99
+ if not self.service:
100
+ logger.error("No active service")
101
+ return False, 0
102
+
103
+ if not self.service.multisig_address:
104
+ logger.error("Service has no multisig address")
105
+ return False, 0
106
+
107
+ if not self.olas_config.withdrawal_address:
108
+ logger.error("No withdrawal address configured in OlasConfig")
109
+ return False, 0
110
+
111
+ multisig_address = str(self.service.multisig_address)
112
+ withdrawal_address = str(self.olas_config.withdrawal_address)
113
+
114
+ # Get OLAS balance of the Safe
115
+ olas_token = ERC20Contract(
116
+ str(OLAS_TOKEN_ADDRESS_GNOSIS),
117
+ chain_name=self.chain_name,
118
+ )
119
+
120
+ olas_balance = olas_token.balance_of_wei(multisig_address)
121
+ if olas_balance == 0:
122
+ logger.info("No OLAS balance to withdraw")
123
+ return False, 0
124
+
125
+ olas_amount = olas_balance / 1e18
126
+ logger.info(
127
+ f"Withdrawing {olas_amount:.4f} OLAS from {multisig_address} to {withdrawal_address}"
128
+ )
129
+
130
+ # Transfer from Safe to withdrawal address
131
+ tx_hash = self.wallet.send(
132
+ from_address_or_tag=multisig_address,
133
+ to_address_or_tag=withdrawal_address,
134
+ amount_wei=olas_balance,
135
+ token_address_or_name=str(OLAS_TOKEN_ADDRESS_GNOSIS),
136
+ chain_name=self.chain_name,
137
+ )
138
+
139
+ if not tx_hash:
140
+ logger.error("Failed to transfer OLAS")
141
+ return False, 0
142
+
143
+ logger.info(f"Withdrew {olas_amount:.4f} OLAS to {withdrawal_address}")
144
+ return True, olas_amount
145
+
146
+ def drain_service(
147
+ self,
148
+ target_address: Optional[str] = None,
149
+ claim_rewards: bool = True,
150
+ ) -> Dict[str, Dict[str, float]]:
151
+ """Drain all service accounts to a target address.
152
+
153
+ This method:
154
+ 1. Claims any pending staking rewards (if staked and claim_rewards=True)
155
+ 2. Drains the Safe (multisig) - native + OLAS tokens
156
+ 3. Drains the Agent account - native + OLAS tokens
157
+ 4. Drains the Owner account - native + OLAS tokens
158
+
159
+ All assets are transferred to the target address (defaults to master account).
160
+
161
+ Args:
162
+ target_address: Address to receive drained funds. Defaults to master account.
163
+ claim_rewards: Whether to claim staking rewards before draining.
164
+
165
+ Returns:
166
+ Dict with drained amounts per account.
167
+
168
+ """
169
+ if not self.service:
170
+ logger.error("No active service")
171
+ return {}
172
+
173
+ target = target_address or self.wallet.master_account.address
174
+ chain = self.chain_name
175
+ drained: Dict[str, Any] = {}
176
+
177
+ logger.info(f"Draining service {self.service.key} to {target}")
178
+
179
+ # Step 1: Claim rewards if staked
180
+ claimed_rewards = self._claim_rewards_if_needed(claim_rewards)
181
+
182
+ # Step 2: Drain the Safe
183
+ safe_result = self._drain_safe_account(target, chain, claimed_rewards)
184
+ if safe_result:
185
+ drained["safe"] = safe_result
186
+
187
+ # Step 3: Drain the Agent account
188
+ agent_result = self._drain_agent_account(target, chain)
189
+ if agent_result:
190
+ drained["agent"] = agent_result
191
+
192
+ # Step 4: Drain the Owner account
193
+ owner_result = self._drain_owner_account(target, chain)
194
+ if owner_result:
195
+ drained["owner"] = owner_result
196
+
197
+ # Handle partial success (rewards claimed but no drain)
198
+ if not drained and claimed_rewards > 0:
199
+ logger.info("Drain returned empty but rewards were claimed. Reporting partial success.")
200
+ drained["safe_rewards_only"] = {"olas": claimed_rewards / 1e18}
201
+
202
+ logger.info(f"Drain complete. Accounts drained: {list(drained.keys())}")
203
+ return drained
204
+
205
+ def _claim_rewards_if_needed(self, claim_rewards: bool) -> int:
206
+ """Claim rewards if applicable."""
207
+ if claim_rewards and self.service.staking_contract_address:
208
+ try:
209
+ success, amount = self.claim_rewards()
210
+ if success and amount > 0:
211
+ logger.info(f"Claimed {amount / 1e18:.4f} OLAS rewards")
212
+ return amount
213
+ except Exception as e:
214
+ logger.warning(f"Could not claim rewards: {e}")
215
+ return 0
216
+
217
+ def _drain_safe_account(self, target: str, chain: str, claimed_rewards: int) -> Optional[Any]:
218
+ """Drain the Safe account with retry logic for rewards."""
219
+ if not self.service.multisig_address:
220
+ return None
221
+
222
+ safe_addr = str(self.service.multisig_address)
223
+ logger.info(f"Attempting to drain Safe: {safe_addr}")
224
+
225
+ # Retry loop if we claimed rewards to allow for RPC indexing
226
+ max_retries = 6 if claimed_rewards > 0 else 1
227
+
228
+ for attempt in range(max_retries):
229
+ try:
230
+ result = self.wallet.drain(
231
+ from_address_or_tag=safe_addr,
232
+ to_address_or_tag=target,
233
+ chain_name=chain,
234
+ )
235
+ logger.info(f"Safe drain result (attempt {attempt + 1}): {result}")
236
+
237
+ normalized_result = self._normalize_drain_result(result)
238
+ if normalized_result:
239
+ logger.info(f"Drained Safe: {normalized_result}")
240
+ return normalized_result
241
+
242
+ if attempt < max_retries - 1:
243
+ logger.info(
244
+ f"Waiting for rewards to appear in balance (attempt {attempt + 1})..."
245
+ )
246
+ import time
247
+
248
+ time.sleep(3)
249
+
250
+ except Exception as e:
251
+ logger.warning(f"Could not drain Safe: {e}")
252
+ import traceback
253
+
254
+ logger.warning(f"Safe traceback: {traceback.format_exc()}")
255
+ if attempt < max_retries - 1:
256
+ import time
257
+
258
+ time.sleep(3)
259
+ return None
260
+
261
+ def _drain_agent_account(self, target: str, chain: str) -> Optional[Any]:
262
+ """Drain the Agent account."""
263
+ if not self.service.agent_address:
264
+ return None
265
+
266
+ agent_addr = str(self.service.agent_address)
267
+ logger.info(f"Attempting to drain Agent: {agent_addr}")
268
+ try:
269
+ result = self.wallet.drain(
270
+ from_address_or_tag=agent_addr,
271
+ to_address_or_tag=target,
272
+ chain_name=chain,
273
+ )
274
+ logger.info(f"Agent drain result: {result}")
275
+ normalized = self._normalize_drain_result(result)
276
+ if normalized:
277
+ logger.info(f"Drained Agent: {normalized}")
278
+ return normalized
279
+ else:
280
+ logger.warning("Agent drain returned None/empty")
281
+ except Exception as e:
282
+ logger.warning(f"Could not drain Agent: {e}")
283
+ import traceback
284
+
285
+ logger.warning(f"Agent traceback: {traceback.format_exc()}")
286
+ return None
287
+
288
+ def _drain_owner_account(self, target: str, chain: str) -> Optional[Any]:
289
+ """Drain the Owner account."""
290
+ if not self.service.service_owner_address:
291
+ return None
292
+
293
+ owner_addr = str(self.service.service_owner_address)
294
+
295
+ # Skip if owner == target (owner is already the destination, e.g., master)
296
+ if owner_addr.lower() == target.lower():
297
+ logger.info("Skipping owner drain: owner is already the target address")
298
+ return None
299
+
300
+ logger.info(f"Attempting to drain Owner: {owner_addr}")
301
+ try:
302
+ result = self.wallet.drain(
303
+ from_address_or_tag=owner_addr,
304
+ to_address_or_tag=target,
305
+ chain_name=chain,
306
+ )
307
+ logger.info(f"Owner drain result: {result}")
308
+ normalized = self._normalize_drain_result(result)
309
+ if normalized:
310
+ logger.info(f"Drained Owner: {normalized}")
311
+ return normalized
312
+ else:
313
+ logger.warning("Owner drain returned None/empty")
314
+ except Exception as e:
315
+ logger.warning(f"Could not drain Owner: {e}")
316
+ import traceback
317
+
318
+ logger.warning(f"Owner traceback: {traceback.format_exc()}")
319
+ return None
320
+
321
+ def _normalize_drain_result(self, result: Any) -> Any:
322
+ """Normalize the result from wallet.drain to a transaction hash string or dict."""
323
+ if not result:
324
+ return None
325
+
326
+ # Handle Tuple[bool, dict] from EOA/TransactionService
327
+ if isinstance(result, tuple) and len(result) >= 2:
328
+ success, receipt = result
329
+ if success:
330
+ tx_hash = receipt.get("transactionHash")
331
+ if hasattr(tx_hash, "hex"):
332
+ return tx_hash.hex()
333
+ return str(tx_hash)
334
+ return None
335
+
336
+ return result