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,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
|