iwa 0.0.32__py3-none-any.whl → 0.0.58__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.
- iwa/core/chain/interface.py +116 -8
- iwa/core/chain/models.py +15 -3
- iwa/core/chain/rate_limiter.py +54 -12
- iwa/core/cli.py +1 -1
- iwa/core/ipfs.py +24 -2
- iwa/core/keys.py +59 -15
- iwa/core/models.py +60 -13
- iwa/core/pricing.py +24 -2
- iwa/core/secrets.py +27 -0
- iwa/core/services/account.py +1 -1
- iwa/core/services/balance.py +0 -22
- iwa/core/services/safe.py +64 -43
- iwa/core/services/safe_executor.py +316 -0
- iwa/core/services/transaction.py +11 -1
- iwa/core/services/transfer/erc20.py +14 -2
- iwa/core/services/transfer/native.py +14 -31
- iwa/core/services/transfer/swap.py +1 -0
- iwa/core/tests/test_gnosis_fee.py +87 -0
- iwa/core/tests/test_ipfs.py +85 -0
- iwa/core/tests/test_pricing.py +65 -0
- iwa/core/tests/test_regression_fixes.py +100 -0
- iwa/core/wallet.py +3 -3
- iwa/plugins/gnosis/cow/quotes.py +2 -2
- iwa/plugins/gnosis/cow/swap.py +18 -32
- iwa/plugins/gnosis/tests/test_cow.py +19 -10
- iwa/plugins/olas/importer.py +5 -7
- iwa/plugins/olas/models.py +0 -3
- iwa/plugins/olas/service_manager/drain.py +16 -7
- iwa/plugins/olas/service_manager/lifecycle.py +15 -4
- iwa/plugins/olas/service_manager/staking.py +4 -4
- iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
- iwa/plugins/olas/tests/test_olas_archiving.py +73 -0
- iwa/plugins/olas/tests/test_olas_view.py +5 -1
- iwa/plugins/olas/tests/test_service_manager.py +7 -7
- iwa/plugins/olas/tests/test_service_manager_errors.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_flows.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
- iwa/tools/drain_accounts.py +60 -0
- iwa/tools/list_contracts.py +2 -0
- iwa/tui/screens/wallets.py +2 -2
- iwa/web/routers/accounts.py +1 -1
- iwa/web/static/app.js +21 -9
- iwa/web/static/style.css +4 -0
- iwa/web/tests/test_web_endpoints.py +2 -2
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/METADATA +6 -3
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/RECORD +64 -54
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/WHEEL +1 -1
- tests/test_balance_service.py +0 -41
- tests/test_chain.py +13 -4
- tests/test_cli.py +2 -2
- tests/test_drain_coverage.py +12 -6
- tests/test_keys.py +23 -23
- tests/test_rate_limiter.py +2 -2
- tests/test_rate_limiter_retry.py +108 -0
- tests/test_rpc_rate_limit.py +33 -0
- tests/test_rpc_rotation.py +55 -7
- tests/test_safe_coverage.py +37 -23
- tests/test_safe_executor.py +335 -0
- tests/test_safe_integration.py +148 -0
- tests/test_safe_service.py +1 -1
- tests/test_transfer_swap_unit.py +5 -1
- tests/test_pricing.py +0 -160
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/top_level.txt +0 -0
iwa/plugins/olas/models.py
CHANGED
|
@@ -134,8 +134,5 @@ class OlasConfig(BaseModel):
|
|
|
134
134
|
target = multisig_address.lower()
|
|
135
135
|
for service in self.services.values():
|
|
136
136
|
if service.multisig_address and str(service.multisig_address).lower() == target:
|
|
137
|
-
# The following line is from the Code Edit, but it does not fit syntactically here.
|
|
138
|
-
# It appears to be from a different file (decoder.py) as indicated by the instruction.
|
|
139
|
-
# args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
|
|
140
137
|
return service
|
|
141
138
|
return None
|
|
@@ -107,12 +107,18 @@ class DrainManagerMixin:
|
|
|
107
107
|
logger.error("Service has no multisig address")
|
|
108
108
|
return False, 0
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
withdrawal_address = (
|
|
111
|
+
str(self.olas_config.withdrawal_address)
|
|
112
|
+
if self.olas_config.withdrawal_address
|
|
113
|
+
else str(self.wallet.master_account.address)
|
|
114
|
+
)
|
|
114
115
|
multisig_address = str(self.service.multisig_address)
|
|
115
|
-
|
|
116
|
+
|
|
117
|
+
if not self.olas_config.withdrawal_address:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"No withdrawal address configured. Defaulting to master account: "
|
|
120
|
+
f"{withdrawal_address}"
|
|
121
|
+
)
|
|
116
122
|
|
|
117
123
|
# Get OLAS balance of the Safe
|
|
118
124
|
olas_token = ERC20Contract(
|
|
@@ -126,8 +132,11 @@ class DrainManagerMixin:
|
|
|
126
132
|
return False, 0
|
|
127
133
|
|
|
128
134
|
olas_amount = olas_balance / 1e18
|
|
135
|
+
withdrawal_tag = self.wallet.get_tag_by_address(withdrawal_address) or withdrawal_address
|
|
136
|
+
multisig_tag = self.wallet.get_tag_by_address(multisig_address) or multisig_address
|
|
137
|
+
|
|
129
138
|
logger.info(
|
|
130
|
-
f"Withdrawing {olas_amount:.4f} OLAS from {
|
|
139
|
+
f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}"
|
|
131
140
|
)
|
|
132
141
|
|
|
133
142
|
# Transfer from Safe to withdrawal address
|
|
@@ -143,7 +152,7 @@ class DrainManagerMixin:
|
|
|
143
152
|
logger.error("Failed to transfer OLAS")
|
|
144
153
|
return False, 0
|
|
145
154
|
|
|
146
|
-
logger.info(f"Withdrew {olas_amount:.4f} OLAS to {
|
|
155
|
+
logger.info(f"Withdrew {olas_amount:.4f} OLAS to {withdrawal_tag}")
|
|
147
156
|
return True, olas_amount
|
|
148
157
|
|
|
149
158
|
def drain_service(
|
|
@@ -632,7 +632,7 @@ class LifecycleManagerMixin:
|
|
|
632
632
|
# Use service_name for consistency with Safe naming
|
|
633
633
|
agent_tag = f"{self.service.service_name}_agent"
|
|
634
634
|
try:
|
|
635
|
-
agent_account = self.wallet.key_storage.
|
|
635
|
+
agent_account = self.wallet.key_storage.generate_new_account(agent_tag)
|
|
636
636
|
agent_account_address = agent_account.address
|
|
637
637
|
logger.info(f"Created new agent account: {agent_account_address}")
|
|
638
638
|
|
|
@@ -855,16 +855,27 @@ class LifecycleManagerMixin:
|
|
|
855
855
|
_, agent_instances = self.registry.call("getAgentInstances", self.service.service_id)
|
|
856
856
|
service_info = self.registry.get_service(self.service.service_id)
|
|
857
857
|
threshold = service_info["threshold"]
|
|
858
|
+
# Store the multisig in the wallet with tag
|
|
859
|
+
multisig_tag = f"{self.service.service_name}_multisig"
|
|
860
|
+
|
|
861
|
+
# ARCHIVING LOGIC: If tag is already taken by a different address, rename the old one
|
|
862
|
+
existing = self.wallet.key_storage.find_stored_account(multisig_tag)
|
|
863
|
+
if existing and existing.address != multisig_address:
|
|
864
|
+
archive_tag = f"{multisig_tag}_old_{existing.address[:6]}"
|
|
865
|
+
logger.info(f"[DEPLOY] Archiving old multisig: {multisig_tag} -> {archive_tag}")
|
|
866
|
+
try:
|
|
867
|
+
self.wallet.key_storage.rename_account(existing.address, archive_tag)
|
|
868
|
+
except Exception as ex:
|
|
869
|
+
logger.warning(f"[DEPLOY] Failed to rename old multisig (collision?): {ex}")
|
|
858
870
|
|
|
859
871
|
safe_account = StoredSafeAccount(
|
|
860
|
-
tag=
|
|
872
|
+
tag=multisig_tag,
|
|
861
873
|
address=multisig_address,
|
|
862
874
|
chains=[self.chain_name],
|
|
863
875
|
threshold=threshold,
|
|
864
876
|
signers=agent_instances,
|
|
865
877
|
)
|
|
866
|
-
self.wallet.key_storage.
|
|
867
|
-
self.wallet.key_storage.save()
|
|
878
|
+
self.wallet.key_storage.register_account(safe_account)
|
|
868
879
|
logger.debug("[DEPLOY] Registered multisig in wallet")
|
|
869
880
|
except Exception as e:
|
|
870
881
|
logger.warning(f"[DEPLOY] Failed to register multisig in wallet: {e}")
|
|
@@ -211,14 +211,14 @@ class StakingManagerMixin:
|
|
|
211
211
|
# Helper to safely get min_staking_duration
|
|
212
212
|
try:
|
|
213
213
|
min_duration = staking.min_staking_duration
|
|
214
|
-
logger.
|
|
214
|
+
logger.debug(f"min_staking_duration: {min_duration}")
|
|
215
215
|
except Exception as e:
|
|
216
216
|
logger.error(f"Failed to get min_staking_duration: {e}")
|
|
217
217
|
min_duration = 0
|
|
218
218
|
|
|
219
219
|
unstake_at = None
|
|
220
220
|
ts_start = info.get("ts_start", 0)
|
|
221
|
-
logger.
|
|
221
|
+
logger.debug(f"ts_start: {ts_start}")
|
|
222
222
|
|
|
223
223
|
if ts_start > 0:
|
|
224
224
|
try:
|
|
@@ -227,12 +227,12 @@ class StakingManagerMixin:
|
|
|
227
227
|
unstake_ts,
|
|
228
228
|
tz=timezone.utc,
|
|
229
229
|
).isoformat()
|
|
230
|
-
logger.
|
|
230
|
+
logger.debug(f"unstake_available_at: {unstake_at} (ts={unstake_ts})")
|
|
231
231
|
except Exception as e:
|
|
232
232
|
logger.error(f"calc error: {e}")
|
|
233
233
|
pass
|
|
234
234
|
else:
|
|
235
|
-
logger.
|
|
235
|
+
logger.debug("ts_start is 0, cannot calculate unstake time")
|
|
236
236
|
|
|
237
237
|
return unstake_at, ts_start, min_duration
|
|
238
238
|
|
|
@@ -296,6 +296,12 @@ def test_import_key_success(importer):
|
|
|
296
296
|
importer.key_storage.find_stored_account.return_value = None
|
|
297
297
|
importer.key_storage.accounts = {}
|
|
298
298
|
|
|
299
|
+
# Define side effect to update accounts dict when register_account is called
|
|
300
|
+
def mock_register(acc):
|
|
301
|
+
importer.key_storage.accounts[acc.address] = acc
|
|
302
|
+
|
|
303
|
+
importer.key_storage.register_account.side_effect = mock_register
|
|
304
|
+
|
|
299
305
|
with patch("iwa.core.keys.EncryptedAccount.encrypt_private_key") as mock_enc:
|
|
300
306
|
mock_enc.return_value = MagicMock(address=addr)
|
|
301
307
|
success, msg = importer._import_key(key, "my_service")
|
|
@@ -310,10 +316,17 @@ def test_import_safe_success(importer):
|
|
|
310
316
|
importer.key_storage.find_stored_account.return_value = None
|
|
311
317
|
importer.key_storage.accounts = {}
|
|
312
318
|
|
|
319
|
+
# Define side effect to update accounts dict when register_account is called
|
|
320
|
+
def mock_register(acc):
|
|
321
|
+
importer.key_storage.accounts[acc.address] = acc
|
|
322
|
+
|
|
323
|
+
importer.key_storage.register_account.side_effect = mock_register
|
|
324
|
+
|
|
313
325
|
success, msg = importer._import_safe(safe_address, service_name="my_service")
|
|
314
326
|
assert success is True
|
|
315
327
|
assert msg == "ok"
|
|
316
328
|
assert safe_address in importer.key_storage.accounts
|
|
329
|
+
assert importer.key_storage.accounts[safe_address].tag == "my_service_multisig"
|
|
317
330
|
|
|
318
331
|
|
|
319
332
|
def test_import_service_config_success(importer):
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for Olas multisig archiving logic."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
from iwa.core.models import StoredAccount
|
|
7
|
+
from iwa.plugins.olas.contracts.service import ServiceState
|
|
8
|
+
from iwa.plugins.olas.models import Service
|
|
9
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestOlasArchiving(unittest.TestCase):
|
|
13
|
+
"""Test multisig archiving logic during deployment."""
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test fixtures."""
|
|
17
|
+
self.mock_wallet = MagicMock()
|
|
18
|
+
# Mock KeyStorage
|
|
19
|
+
self.mock_key_storage = MagicMock()
|
|
20
|
+
self.mock_wallet.key_storage = self.mock_key_storage
|
|
21
|
+
|
|
22
|
+
# Initialize ServiceManager
|
|
23
|
+
with patch("iwa.core.models.Config"), \
|
|
24
|
+
patch("iwa.plugins.olas.service_manager.base.ChainInterfaces"), \
|
|
25
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache"), \
|
|
26
|
+
patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract"):
|
|
27
|
+
self.sm = ServiceManager(self.mock_wallet)
|
|
28
|
+
self.sm.service = Service(service_name="trader_psi", chain_name="gnosis", service_id=1)
|
|
29
|
+
self.sm.chain_name = "gnosis"
|
|
30
|
+
|
|
31
|
+
def test_multisig_archiving_logic(self):
|
|
32
|
+
"""Test that old multisig is archived if tag is taken by different address."""
|
|
33
|
+
multisig_tag = "trader_psi_multisig"
|
|
34
|
+
old_address = "0x1111111111111111111111111111111111111111"
|
|
35
|
+
new_address = "0x2222222222222222222222222222222222222222"
|
|
36
|
+
|
|
37
|
+
# 1. Simulate existing multisig in wallet with same tag
|
|
38
|
+
existing_acc = StoredAccount(address=old_address, tag=multisig_tag)
|
|
39
|
+
self.mock_key_storage.find_stored_account.return_value = existing_acc
|
|
40
|
+
|
|
41
|
+
# 2. Mock required contract calls for deploy()
|
|
42
|
+
self.sm.registry.get_service.return_value = {
|
|
43
|
+
"state": ServiceState.FINISHED_REGISTRATION, # DEPLOYED
|
|
44
|
+
"security_deposit": 0,
|
|
45
|
+
"multisig": new_address,
|
|
46
|
+
"threshold": 1,
|
|
47
|
+
"configHash": b"\x00" * 32
|
|
48
|
+
}
|
|
49
|
+
self.sm.registry.call.return_value = (None, []) # getAgentInstances
|
|
50
|
+
|
|
51
|
+
# 3. Trigger deploy (archiving happens here)
|
|
52
|
+
with patch.object(self.mock_wallet, "sign_and_send_transaction", return_value=(True, {"status": 1})), \
|
|
53
|
+
patch.object(self.sm.registry, "extract_events", return_value=[
|
|
54
|
+
{"name": "DeployService", "args": {}},
|
|
55
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": new_address}}
|
|
56
|
+
]), \
|
|
57
|
+
patch("iwa.plugins.olas.service_manager.lifecycle.get_tx_hash", return_value="0xhash"), \
|
|
58
|
+
patch("iwa.core.models.StoredSafeAccount") as mock_safe_cls:
|
|
59
|
+
|
|
60
|
+
self.sm.deploy(fund_multisig=False)
|
|
61
|
+
|
|
62
|
+
# 4. Verify rename_account was called for the old address
|
|
63
|
+
archive_tag = f"{multisig_tag}_old_{old_address[:6]}"
|
|
64
|
+
self.mock_key_storage.rename_account.assert_called_with(old_address, archive_tag)
|
|
65
|
+
|
|
66
|
+
# 5. Verify new account constructor was called with correct params
|
|
67
|
+
mock_safe_cls.assert_called()
|
|
68
|
+
_, kwargs = mock_safe_cls.call_args
|
|
69
|
+
self.assertEqual(kwargs["address"], new_address)
|
|
70
|
+
self.assertEqual(kwargs["tag"], multisig_tag)
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
unittest.main()
|
|
@@ -147,21 +147,25 @@ async def test_olas_view_actions(mock_wallet, mock_olas_config):
|
|
|
147
147
|
|
|
148
148
|
app = OlasTestApp(mock_wallet)
|
|
149
149
|
async with app.run_test() as pilot:
|
|
150
|
-
|
|
150
|
+
# Wait for initial load to finish
|
|
151
|
+
await pilot.pause(0.5)
|
|
151
152
|
|
|
152
153
|
# 1. Test Claim
|
|
153
154
|
with patch.object(OlasView, "claim_rewards") as mock_claim:
|
|
154
155
|
await pilot.click("#claim-gnosis_1")
|
|
156
|
+
await pilot.pause()
|
|
155
157
|
mock_claim.assert_called_with("gnosis:1")
|
|
156
158
|
|
|
157
159
|
# 2. Test Unstake
|
|
158
160
|
with patch.object(OlasView, "unstake_service") as mock_unstake:
|
|
159
161
|
await pilot.click("#unstake-gnosis_1")
|
|
162
|
+
await pilot.pause()
|
|
160
163
|
mock_unstake.assert_called_with("gnosis:1")
|
|
161
164
|
|
|
162
165
|
# 3. Test Checkpoint
|
|
163
166
|
with patch.object(OlasView, "checkpoint_service") as mock_checkpoint:
|
|
164
167
|
await pilot.click("#checkpoint-gnosis_1")
|
|
168
|
+
await pilot.pause()
|
|
165
169
|
mock_checkpoint.assert_called_with("gnosis:1")
|
|
166
170
|
|
|
167
171
|
|
|
@@ -65,11 +65,11 @@ def mock_wallet():
|
|
|
65
65
|
wallet.master_account.address = "0x1234567890123456789012345678901234567890"
|
|
66
66
|
wallet.key_storage = MagicMock()
|
|
67
67
|
wallet.key_storage.get_account.return_value = None # Default
|
|
68
|
-
# Mock
|
|
68
|
+
# Mock generate_new_account which returns a StoredAccount or similar
|
|
69
69
|
new_acc = MagicMock()
|
|
70
70
|
new_acc.address = "0x0987654321098765432109876543210987654321"
|
|
71
|
-
wallet.key_storage.
|
|
72
|
-
wallet.key_storage.
|
|
71
|
+
wallet.key_storage.generate_new_account.return_value = new_acc
|
|
72
|
+
wallet.key_storage.generate_new_account.return_value = new_acc
|
|
73
73
|
# Mock account_service
|
|
74
74
|
wallet.account_service = MagicMock()
|
|
75
75
|
wallet.account_service.get_tag_by_address.return_value = "mock_tag"
|
|
@@ -219,7 +219,7 @@ def test_register_agent_success(service_manager, mock_wallet):
|
|
|
219
219
|
"security_deposit": 50000000000000000000,
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
#
|
|
222
|
+
# generate_new_account is already mocked
|
|
223
223
|
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
224
224
|
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
225
225
|
service_manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
|
|
@@ -331,7 +331,7 @@ def test_register_agent_with_existing_address(service_manager, mock_wallet):
|
|
|
331
331
|
assert service_manager.register_agent(agent_address=existing_agent) is True
|
|
332
332
|
assert service_manager.service.agent_address == TEST_EXISTING_AGENT_ADDR
|
|
333
333
|
# Should NOT create a new account
|
|
334
|
-
mock_wallet.key_storage.
|
|
334
|
+
mock_wallet.key_storage.generate_new_account.assert_not_called()
|
|
335
335
|
# Should NOT fund the agent (only for new accounts)
|
|
336
336
|
mock_wallet.send.assert_not_called()
|
|
337
337
|
|
|
@@ -349,7 +349,7 @@ def test_register_agent_creates_new_if_none(service_manager, mock_wallet):
|
|
|
349
349
|
|
|
350
350
|
assert service_manager.register_agent() is True
|
|
351
351
|
# Should create a new account
|
|
352
|
-
mock_wallet.key_storage.
|
|
352
|
+
mock_wallet.key_storage.generate_new_account.assert_called()
|
|
353
353
|
# Should fund the new agent
|
|
354
354
|
mock_wallet.send.assert_called()
|
|
355
355
|
|
|
@@ -661,7 +661,7 @@ def test_spin_up_with_existing_agent(service_manager, mock_wallet):
|
|
|
661
661
|
existing_agent = TEST_EXISTING_AGENT_ADDR
|
|
662
662
|
assert service_manager.spin_up(agent_address=existing_agent) is True
|
|
663
663
|
# Verify agent address was not newly created
|
|
664
|
-
mock_wallet.key_storage.
|
|
664
|
+
mock_wallet.key_storage.generate_new_account.assert_not_called()
|
|
665
665
|
|
|
666
666
|
|
|
667
667
|
# --- Tests for wind_down ---
|
|
@@ -18,7 +18,7 @@ def mock_wallet():
|
|
|
18
18
|
wallet.master_account.address = VALID_ADDR
|
|
19
19
|
wallet.chain_interface.chain_name = "gnosis"
|
|
20
20
|
wallet.sign_and_send_transaction.return_value = (True, {"transactionHash": b"\x00" * 32})
|
|
21
|
-
wallet.key_storage.
|
|
21
|
+
wallet.key_storage.generate_new_account.return_value.address = VALID_ADDR
|
|
22
22
|
wallet.key_storage.get_account.return_value.address = VALID_ADDR
|
|
23
23
|
return wallet
|
|
24
24
|
|
|
@@ -17,7 +17,7 @@ def mock_wallet():
|
|
|
17
17
|
wallet.key_storage.get_account.return_value.address = (
|
|
18
18
|
"0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
19
19
|
)
|
|
20
|
-
wallet.key_storage.
|
|
20
|
+
wallet.key_storage.generate_new_account.return_value.address = (
|
|
21
21
|
"0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
|
22
22
|
)
|
|
23
23
|
wallet.sign_and_send_transaction.return_value = (True, {"status": 1})
|
|
@@ -197,7 +197,11 @@ def test_withdraw_rewards_no_withdrawal_address(mock_wallet):
|
|
|
197
197
|
)
|
|
198
198
|
manager.olas_config.withdrawal_address = None
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
|
|
201
|
+
mock_erc20 = mock_erc20_cls.return_value
|
|
202
|
+
mock_erc20.balance_of_wei.return_value = 0
|
|
203
|
+
|
|
204
|
+
success, amount = manager.withdraw_rewards()
|
|
201
205
|
|
|
202
206
|
assert success is False
|
|
203
207
|
assert amount == 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
"""Tool to drain specific accounts to a master address."""
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# Ensure src is in pythonpath
|
|
10
|
+
sys.path.append(str(Path(__file__).resolve().parents[2]))
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from iwa.core.constants import SECRETS_PATH
|
|
15
|
+
from iwa.core.wallet import Wallet
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""Run the account draining tool."""
|
|
20
|
+
parser = argparse.ArgumentParser(description="Drain specific accounts to master.")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"tags",
|
|
23
|
+
nargs="+",
|
|
24
|
+
help="List of account tags to drain (or 'all' for all configured accounts)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--chain",
|
|
28
|
+
"-c",
|
|
29
|
+
default="gnosis",
|
|
30
|
+
help="Target chain (default: gnosis)",
|
|
31
|
+
)
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
# Load secrets
|
|
35
|
+
if SECRETS_PATH.exists():
|
|
36
|
+
load_dotenv(SECRETS_PATH, override=True)
|
|
37
|
+
|
|
38
|
+
wallet = Wallet()
|
|
39
|
+
|
|
40
|
+
tags = args.tags
|
|
41
|
+
if "all" in tags:
|
|
42
|
+
# Get all configured accounts except master
|
|
43
|
+
all_accounts = wallet.account_service.get_account_data()
|
|
44
|
+
tags = [acc.tag for acc in all_accounts.values() if acc.tag != "master"]
|
|
45
|
+
logger.info(f"Draining ALL accounts: {tags}")
|
|
46
|
+
|
|
47
|
+
for tag in tags:
|
|
48
|
+
logger.info(f"Processing drain for account: {tag}")
|
|
49
|
+
try:
|
|
50
|
+
tx_hash = wallet.drain(from_address_or_tag=tag, chain_name=args.chain)
|
|
51
|
+
if tx_hash:
|
|
52
|
+
logger.success(f"Drain tx sent for {tag}: {tx_hash}")
|
|
53
|
+
# Optional: link to explorer
|
|
54
|
+
else:
|
|
55
|
+
logger.warning(f"Drain failed or nothing to drain for {tag}")
|
|
56
|
+
except Exception:
|
|
57
|
+
logger.exception(f"Error draining {tag}")
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
iwa/tools/list_contracts.py
CHANGED
|
@@ -101,6 +101,7 @@ def print_table(console, contract_data, chain_name, sort_criterion):
|
|
|
101
101
|
table.add_column("Available Rewards", justify="right", style="yellow")
|
|
102
102
|
table.add_column("Contract Balance", justify="right", style="blue")
|
|
103
103
|
table.add_column("Epoch End (UTC)", justify="right", style="white")
|
|
104
|
+
table.add_column("Epoch End (Local)", justify="right", style="white")
|
|
104
105
|
|
|
105
106
|
for item in contract_data:
|
|
106
107
|
if item.get("error"):
|
|
@@ -113,6 +114,7 @@ def print_table(console, contract_data, chain_name, sort_criterion):
|
|
|
113
114
|
f"{item['rewards_olas']:,.2f} OLAS",
|
|
114
115
|
f"{item['balance_olas']:,.2f} OLAS",
|
|
115
116
|
item["epoch_end"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
117
|
+
item["epoch_end"].astimezone().strftime("%Y-%m-%d %H:%M:%S"),
|
|
116
118
|
)
|
|
117
119
|
console.print(table)
|
|
118
120
|
|
iwa/tui/screens/wallets.py
CHANGED
|
@@ -330,7 +330,7 @@ class WalletsScreen(VerticalScroll):
|
|
|
330
330
|
|
|
331
331
|
def _fetch_single_token_balance(self, address: str, token: str, chain_name: str) -> str:
|
|
332
332
|
"""Fetch a single token balance using BalanceService."""
|
|
333
|
-
val_token = self.wallet.balance_service.
|
|
333
|
+
val_token = self.wallet.balance_service.get_erc20_balance_eth(
|
|
334
334
|
address, token, chain_name
|
|
335
335
|
)
|
|
336
336
|
val_token_str = f"{val_token:.4f}" if val_token is not None else "-"
|
|
@@ -442,7 +442,7 @@ class WalletsScreen(VerticalScroll):
|
|
|
442
442
|
def handler(tag):
|
|
443
443
|
if tag is not None:
|
|
444
444
|
tag = tag or f"Account {len(self.wallet.key_storage.accounts) + 1}"
|
|
445
|
-
self.wallet.key_storage.
|
|
445
|
+
self.wallet.key_storage.generate_new_account(tag)
|
|
446
446
|
self.notify(f"Created new EOA: {escape(tag)}")
|
|
447
447
|
self.refresh_accounts()
|
|
448
448
|
|
iwa/web/routers/accounts.py
CHANGED
|
@@ -68,7 +68,7 @@ def get_accounts(
|
|
|
68
68
|
def create_eoa(request: Request, req: AccountCreateRequest, auth: bool = Depends(verify_auth)):
|
|
69
69
|
"""Create a new EOA account with the given tag."""
|
|
70
70
|
try:
|
|
71
|
-
wallet.key_storage.
|
|
71
|
+
wallet.key_storage.generate_new_account(req.tag)
|
|
72
72
|
return {"status": "success"}
|
|
73
73
|
except Exception as e:
|
|
74
74
|
raise HTTPException(status_code=400, detail=str(e)) from None
|
iwa/web/static/app.js
CHANGED
|
@@ -2096,13 +2096,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2096
2096
|
<span class="value ${
|
|
2097
2097
|
isLoading
|
|
2098
2098
|
? ""
|
|
2099
|
-
:
|
|
2100
|
-
? "
|
|
2101
|
-
:
|
|
2102
|
-
? "
|
|
2103
|
-
: "
|
|
2099
|
+
: staking.staking_state === "EVICTED"
|
|
2100
|
+
? "evicted"
|
|
2101
|
+
: isStaked
|
|
2102
|
+
? "staked"
|
|
2103
|
+
: service.state === "DEPLOYED"
|
|
2104
|
+
? "deployed"
|
|
2105
|
+
: "not-staked"
|
|
2104
2106
|
}">
|
|
2105
|
-
${
|
|
2107
|
+
${
|
|
2108
|
+
isLoading
|
|
2109
|
+
? '<span class="cell-spinner"></span>'
|
|
2110
|
+
: (service.state || "UNKNOWN") +
|
|
2111
|
+
(staking.staking_state
|
|
2112
|
+
? `, ${staking.staking_state.replace(/_/g, " ")}`
|
|
2113
|
+
: isStaked
|
|
2114
|
+
? ", STAKED"
|
|
2115
|
+
: ", NOT STAKED")
|
|
2116
|
+
}
|
|
2106
2117
|
</span>
|
|
2107
2118
|
</div>
|
|
2108
2119
|
<div class="staking-row">
|
|
@@ -2638,11 +2649,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2638
2649
|
|
|
2639
2650
|
// Show select, hide spinner
|
|
2640
2651
|
select.style.display = "";
|
|
2641
|
-
spinnerDiv.
|
|
2652
|
+
spinnerDiv.classList.add("hidden");
|
|
2642
2653
|
} catch (err) {
|
|
2643
2654
|
select.innerHTML = '<option value="">Error loading contracts</option>';
|
|
2644
2655
|
select.style.display = "";
|
|
2645
|
-
spinnerDiv.
|
|
2656
|
+
spinnerDiv.classList.add("hidden");
|
|
2646
2657
|
confirmBtn.disabled = false;
|
|
2647
2658
|
}
|
|
2648
2659
|
};
|
|
@@ -2699,7 +2710,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2699
2710
|
|
|
2700
2711
|
// Load staking contracts
|
|
2701
2712
|
contractSelect.style.display = "none";
|
|
2702
|
-
spinnerDiv.
|
|
2713
|
+
spinnerDiv.classList.remove("hidden");
|
|
2714
|
+
spinnerDiv.style.display = "block"; // Ensure display block for visibility
|
|
2703
2715
|
spinnerDiv.innerHTML =
|
|
2704
2716
|
'<span class="loading-spinner"></span> Loading contracts...';
|
|
2705
2717
|
submitBtn.disabled = true;
|
iwa/web/static/style.css
CHANGED
|
@@ -284,7 +284,7 @@ def test_safe_create_request_validation(client):
|
|
|
284
284
|
|
|
285
285
|
def test_create_eoa_success(client):
|
|
286
286
|
"""Cover create_eoa success (lines 308-315)."""
|
|
287
|
-
wallet.key_storage.
|
|
287
|
+
wallet.key_storage.generate_new_account = MagicMock()
|
|
288
288
|
|
|
289
289
|
response = client.post("/api/accounts/eoa", json={"tag": "my_wallet"})
|
|
290
290
|
assert response.status_code == 200
|
|
@@ -293,7 +293,7 @@ def test_create_eoa_success(client):
|
|
|
293
293
|
|
|
294
294
|
def test_create_eoa_error(client):
|
|
295
295
|
"""Cover create_eoa error (lines 314-315)."""
|
|
296
|
-
wallet.key_storage.
|
|
296
|
+
wallet.key_storage.generate_new_account = MagicMock(side_effect=Exception("Tag exists"))
|
|
297
297
|
|
|
298
298
|
response = client.post("/api/accounts/eoa", json={"tag": "existing"})
|
|
299
299
|
assert response.status_code == 400
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iwa
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.58
|
|
4
4
|
Summary: A secure, modular, and plugin-based framework for crypto agents and ops
|
|
5
5
|
Requires-Python: <4.0,>=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -17,6 +17,7 @@ Requires-Dist: tomli-w>=1.2.0
|
|
|
17
17
|
Requires-Dist: typer>=0.19.2
|
|
18
18
|
Requires-Dist: web3>=7.13.0
|
|
19
19
|
Requires-Dist: pyyaml<7.0.0,>=6.0.3
|
|
20
|
+
Requires-Dist: ruamel.yaml>=0.18.0
|
|
20
21
|
Requires-Dist: safe-eth-py>=7.14.0
|
|
21
22
|
Requires-Dist: twine>=6.2.0
|
|
22
23
|
Requires-Dist: cowdao-cowpy>=1.0.1
|
|
@@ -82,7 +83,7 @@ iwa/
|
|
|
82
83
|
├── core/ # Core wallet functionality
|
|
83
84
|
│ ├── keys.py # KeyStorage - Encrypted key management
|
|
84
85
|
│ ├── wallet.py # Wallet - High-level interface
|
|
85
|
-
│ ├── chain
|
|
86
|
+
│ ├── chain/ # Blockchain interface with rate limiting
|
|
86
87
|
│ ├── services/ # Service layer (accounts, balances, transactions)
|
|
87
88
|
│ └── contracts/ # Contract abstractions (ERC20, Safe)
|
|
88
89
|
├── plugins/ # Protocol integrations
|
|
@@ -134,6 +135,9 @@ GNOSIS_RPC=https://rpc.gnosis.io,https://gnosis.drpc.org
|
|
|
134
135
|
ETHEREUM_RPC=https://mainnet.infura.io/v3/YOUR_KEY
|
|
135
136
|
BASE_RPC=https://mainnet.base.org
|
|
136
137
|
|
|
138
|
+
# Testing mode (default: true uses Tenderly test RPCs)
|
|
139
|
+
TESTING=false
|
|
140
|
+
|
|
137
141
|
# Optional
|
|
138
142
|
GNOSISSCAN_API_KEY=your_api_key
|
|
139
143
|
COINGECKO_API_KEY=your_api_key
|
|
@@ -150,7 +154,6 @@ just web
|
|
|
150
154
|
|
|
151
155
|
# Use CLI
|
|
152
156
|
iwa wallet list --chain gnosis
|
|
153
|
-
iwa wallet balance <address> --chain gnosis
|
|
154
157
|
```
|
|
155
158
|
|
|
156
159
|
### Running Tests
|