iwa 0.0.33__py3-none-any.whl → 0.0.59__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 (83) hide show
  1. iwa/core/chain/interface.py +130 -11
  2. iwa/core/chain/models.py +15 -3
  3. iwa/core/chain/rate_limiter.py +48 -12
  4. iwa/core/chainlist.py +15 -10
  5. iwa/core/cli.py +4 -1
  6. iwa/core/contracts/cache.py +1 -1
  7. iwa/core/contracts/contract.py +1 -0
  8. iwa/core/contracts/decoder.py +10 -4
  9. iwa/core/http.py +31 -0
  10. iwa/core/ipfs.py +21 -7
  11. iwa/core/keys.py +65 -15
  12. iwa/core/models.py +58 -13
  13. iwa/core/pricing.py +10 -6
  14. iwa/core/rpc_monitor.py +1 -0
  15. iwa/core/secrets.py +27 -0
  16. iwa/core/services/account.py +1 -1
  17. iwa/core/services/balance.py +0 -23
  18. iwa/core/services/safe.py +72 -45
  19. iwa/core/services/safe_executor.py +350 -0
  20. iwa/core/services/transaction.py +43 -13
  21. iwa/core/services/transfer/erc20.py +14 -3
  22. iwa/core/services/transfer/native.py +14 -31
  23. iwa/core/services/transfer/swap.py +1 -0
  24. iwa/core/tests/test_gnosis_fee.py +91 -0
  25. iwa/core/tests/test_ipfs.py +85 -0
  26. iwa/core/tests/test_pricing.py +65 -0
  27. iwa/core/tests/test_regression_fixes.py +97 -0
  28. iwa/core/utils.py +2 -0
  29. iwa/core/wallet.py +6 -4
  30. iwa/plugins/gnosis/cow/quotes.py +2 -2
  31. iwa/plugins/gnosis/cow/swap.py +18 -32
  32. iwa/plugins/gnosis/tests/test_cow.py +19 -10
  33. iwa/plugins/olas/constants.py +15 -5
  34. iwa/plugins/olas/contracts/activity_checker.py +3 -3
  35. iwa/plugins/olas/contracts/staking.py +0 -1
  36. iwa/plugins/olas/events.py +15 -13
  37. iwa/plugins/olas/importer.py +29 -25
  38. iwa/plugins/olas/models.py +0 -3
  39. iwa/plugins/olas/plugin.py +16 -14
  40. iwa/plugins/olas/service_manager/drain.py +16 -9
  41. iwa/plugins/olas/service_manager/lifecycle.py +23 -12
  42. iwa/plugins/olas/service_manager/staking.py +15 -10
  43. iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
  44. iwa/plugins/olas/tests/test_olas_archiving.py +83 -0
  45. iwa/plugins/olas/tests/test_olas_integration.py +49 -29
  46. iwa/plugins/olas/tests/test_olas_view.py +5 -1
  47. iwa/plugins/olas/tests/test_service_manager.py +15 -17
  48. iwa/plugins/olas/tests/test_service_manager_errors.py +6 -5
  49. iwa/plugins/olas/tests/test_service_manager_flows.py +7 -6
  50. iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
  51. iwa/plugins/olas/tests/test_service_staking.py +64 -38
  52. iwa/tools/drain_accounts.py +61 -0
  53. iwa/tools/list_contracts.py +2 -0
  54. iwa/tools/reset_env.py +2 -1
  55. iwa/tools/test_chainlist.py +5 -1
  56. iwa/tui/screens/wallets.py +2 -4
  57. iwa/web/routers/accounts.py +1 -1
  58. iwa/web/routers/olas/services.py +10 -5
  59. iwa/web/static/app.js +21 -9
  60. iwa/web/static/style.css +4 -0
  61. iwa/web/tests/test_web_endpoints.py +2 -2
  62. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/METADATA +6 -3
  63. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/RECORD +82 -71
  64. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/WHEEL +1 -1
  65. tests/test_balance_service.py +0 -43
  66. tests/test_chain.py +13 -5
  67. tests/test_cli.py +2 -2
  68. tests/test_drain_coverage.py +12 -6
  69. tests/test_keys.py +23 -23
  70. tests/test_rate_limiter.py +2 -2
  71. tests/test_rate_limiter_retry.py +103 -0
  72. tests/test_rpc_efficiency.py +4 -1
  73. tests/test_rpc_rate_limit.py +34 -0
  74. tests/test_rpc_rotation.py +59 -11
  75. tests/test_safe_coverage.py +37 -23
  76. tests/test_safe_executor.py +361 -0
  77. tests/test_safe_integration.py +153 -0
  78. tests/test_safe_service.py +1 -1
  79. tests/test_transfer_swap_unit.py +5 -1
  80. tests/test_pricing.py +0 -160
  81. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/entry_points.txt +0 -0
  82. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/licenses/LICENSE +0 -0
  83. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/top_level.txt +0 -0
@@ -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
- await pilot.pause()
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 create_account which returns a StoredAccount or similar
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.create_account.return_value = new_acc
72
- wallet.key_storage.create_account.return_value = new_acc
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"
@@ -104,8 +104,6 @@ def mock_chain_interfaces():
104
104
  yield mock
105
105
 
106
106
 
107
-
108
-
109
107
  @pytest.fixture
110
108
  def service_manager(
111
109
  mock_config,
@@ -113,13 +111,14 @@ def service_manager(
113
111
  mock_registry,
114
112
  mock_manager_contract,
115
113
  mock_chain_interfaces,
116
-
117
114
  mock_olas_config,
118
115
  mock_service,
119
116
  ):
120
117
  """ServiceManager fixture with mocked dependencies."""
121
- with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config, \
122
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache:
118
+ with (
119
+ patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config,
120
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
121
+ ):
123
122
  instance = local_mock_config.return_value
124
123
  instance.plugins = {"olas": mock_olas_config}
125
124
  instance.save_config = MagicMock()
@@ -136,7 +135,6 @@ def service_manager(
136
135
  yield sm
137
136
 
138
137
 
139
-
140
138
  def test_init(service_manager):
141
139
  """Test initialization."""
142
140
  assert service_manager.registry is not None
@@ -179,9 +177,7 @@ def test_create_no_event(service_manager, mock_wallet):
179
177
  mock_wallet.sign_and_send_transaction.return_value = (True, {})
180
178
  service_manager.registry.extract_events.return_value = []
181
179
 
182
- res = service_manager.create(
183
- token_address_or_tag="0x1111111111111111111111111111111111111111"
184
- )
180
+ res = service_manager.create(token_address_or_tag="0x1111111111111111111111111111111111111111")
185
181
  # create() finds no ID, logs error, returns None for service_id.
186
182
  assert res is None
187
183
 
@@ -219,7 +215,7 @@ def test_register_agent_success(service_manager, mock_wallet):
219
215
  "security_deposit": 50000000000000000000,
220
216
  }
221
217
 
222
- # create_account is already mocked
218
+ # generate_new_account is already mocked
223
219
  mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
224
220
  mock_wallet.sign_and_send_transaction.return_value = (True, {})
225
221
  service_manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
@@ -331,7 +327,7 @@ def test_register_agent_with_existing_address(service_manager, mock_wallet):
331
327
  assert service_manager.register_agent(agent_address=existing_agent) is True
332
328
  assert service_manager.service.agent_address == TEST_EXISTING_AGENT_ADDR
333
329
  # Should NOT create a new account
334
- mock_wallet.key_storage.create_account.assert_not_called()
330
+ mock_wallet.key_storage.generate_new_account.assert_not_called()
335
331
  # Should NOT fund the agent (only for new accounts)
336
332
  mock_wallet.send.assert_not_called()
337
333
 
@@ -349,7 +345,7 @@ def test_register_agent_creates_new_if_none(service_manager, mock_wallet):
349
345
 
350
346
  assert service_manager.register_agent() is True
351
347
  # Should create a new account
352
- mock_wallet.key_storage.create_account.assert_called()
348
+ mock_wallet.key_storage.generate_new_account.assert_called()
353
349
  # Should fund the new agent
354
350
  mock_wallet.send.assert_called()
355
351
 
@@ -661,7 +657,7 @@ def test_spin_up_with_existing_agent(service_manager, mock_wallet):
661
657
  existing_agent = TEST_EXISTING_AGENT_ADDR
662
658
  assert service_manager.spin_up(agent_address=existing_agent) is True
663
659
  # Verify agent address was not newly created
664
- mock_wallet.key_storage.create_account.assert_not_called()
660
+ mock_wallet.key_storage.generate_new_account.assert_not_called()
665
661
 
666
662
 
667
663
  # --- Tests for wind_down ---
@@ -911,7 +907,9 @@ def test_activate_registration_token_service_sends_security_deposit_as_value(
911
907
 
912
908
  # Mock balance check to pass
913
909
  mock_wallet.balance_service = MagicMock()
914
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18 # Plenty of balance
910
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = (
911
+ 100 * 10**18
912
+ ) # Plenty of balance
915
913
 
916
914
  # Mock allowance to pass check (return an int)
917
915
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20 # Plenty of allowance
@@ -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.create_account.return_value.address = VALID_ADDR
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
 
@@ -26,10 +26,11 @@ def mock_wallet():
26
26
  @pytest.fixture
27
27
  def mock_manager(mock_wallet):
28
28
  """Setup a ServiceManager with mocked dependencies."""
29
- with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls, \
30
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
31
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
32
-
29
+ with (
30
+ patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls,
31
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
32
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
33
+ ):
33
34
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
34
35
 
35
36
  mock_cfg = mock_cfg_cls.return_value
@@ -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.create_account.return_value.address = (
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})
@@ -237,7 +237,9 @@ def test_activate_registration(
237
237
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
238
238
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
239
239
  @patch("iwa.plugins.olas.service_manager.base.ContractCache")
240
- def test_register_agent(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
240
+ def test_register_agent(
241
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
242
+ ):
241
243
  """Test agent registration flow."""
242
244
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
243
245
  mock_config_inst = mock_config_cls.return_value
@@ -354,7 +356,9 @@ def test_deploy(mock_cache, mock_sm_contract, mock_registry_contract, mock_confi
354
356
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
355
357
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
356
358
  @patch("iwa.plugins.olas.service_manager.base.ContractCache")
357
- def test_terminate(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
359
+ def test_terminate(
360
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
361
+ ):
358
362
  """Test service termination."""
359
363
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
360
364
  # Setup mock service
@@ -457,7 +461,6 @@ def test_stake(mock_cache, mock_sm_contract, mock_registry_contract, mock_config
457
461
  "required_agent_bond": 50000000000000000000,
458
462
  }
459
463
 
460
-
461
464
  success = manager.stake(mock_staking)
462
465
  assert success is True
463
466
  assert manager.service.staking_contract_address == "0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd"
@@ -480,8 +483,6 @@ def test_stake(mock_cache, mock_sm_contract, mock_registry_contract, mock_config
480
483
  assert manager.stake(mock_staking) is False
481
484
  mock_staking.get_service_ids.return_value = []
482
485
 
483
-
484
-
485
486
  # 4. Approve fail
486
487
  mock_wallet.sign_and_send_transaction.return_value = (False, {})
487
488
  assert manager.stake(mock_staking) is False
@@ -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
- success, amount = manager.withdraw_rewards()
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
@@ -33,9 +33,11 @@ def mock_wallet():
33
33
 
34
34
  def test_sm_unstake_not_staked(mock_wallet):
35
35
  """Cover unstake when not staked (lines 736-738)."""
36
- with patch("iwa.core.models.Config"), \
37
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
38
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
36
+ with (
37
+ patch("iwa.core.models.Config"),
38
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
39
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
40
+ ):
39
41
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
40
42
  sm = ServiceManager(mock_wallet)
41
43
  sm.service = Service(
@@ -51,9 +53,11 @@ def test_sm_unstake_not_staked(mock_wallet):
51
53
 
52
54
  def test_sm_unstake_tx_fails(mock_wallet):
53
55
  """Cover unstake transaction failure (lines 766-768)."""
54
- with patch("iwa.core.models.Config"), \
55
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
56
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
56
+ with (
57
+ patch("iwa.core.models.Config"),
58
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
59
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
60
+ ):
57
61
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
58
62
  sm = ServiceManager(mock_wallet)
59
63
  sm.service = Service(
@@ -77,9 +81,11 @@ def test_sm_unstake_tx_fails(mock_wallet):
77
81
 
78
82
  def test_sm_get_staking_status_no_staking_address(mock_wallet):
79
83
  """Cover get_staking_status with no staking address (lines 831)."""
80
- with patch("iwa.core.models.Config"), \
81
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
82
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
84
+ with (
85
+ patch("iwa.core.models.Config"),
86
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
87
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
88
+ ):
83
89
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
84
90
  sm = ServiceManager(mock_wallet)
85
91
  sm.service = Service(
@@ -97,9 +103,11 @@ def test_sm_get_staking_status_no_staking_address(mock_wallet):
97
103
 
98
104
  def test_sm_get_staking_status_with_full_info(mock_wallet):
99
105
  """Cover get_staking_status with complete info (lines 866-891)."""
100
- with patch("iwa.core.models.Config"), \
101
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
102
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
106
+ with (
107
+ patch("iwa.core.models.Config"),
108
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
109
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
110
+ ):
103
111
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
104
112
  sm = ServiceManager(mock_wallet)
105
113
  sm.service = Service(
@@ -142,9 +150,11 @@ def test_sm_get_staking_status_with_full_info(mock_wallet):
142
150
 
143
151
  def test_sm_claim_rewards_no_service(mock_wallet):
144
152
  """Cover claim_rewards with no service (lines 936-938)."""
145
- with patch("iwa.core.models.Config"), \
146
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
147
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
153
+ with (
154
+ patch("iwa.core.models.Config"),
155
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
156
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
157
+ ):
148
158
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
149
159
  sm = ServiceManager(mock_wallet)
150
160
  sm.service = None
@@ -156,9 +166,11 @@ def test_sm_claim_rewards_no_service(mock_wallet):
156
166
 
157
167
  def test_sm_claim_rewards_no_staking_address(mock_wallet):
158
168
  """Cover claim_rewards with no staking address (lines 939-943)."""
159
- with patch("iwa.core.models.Config"), \
160
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
161
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
169
+ with (
170
+ patch("iwa.core.models.Config"),
171
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
172
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
173
+ ):
162
174
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
163
175
  sm = ServiceManager(mock_wallet)
164
176
  sm.service = Service(
@@ -171,9 +183,11 @@ def test_sm_claim_rewards_no_staking_address(mock_wallet):
171
183
 
172
184
  def test_sm_claim_rewards_tx_fails(mock_wallet):
173
185
  """Cover claim_rewards transaction failure (lines 967-968)."""
174
- with patch("iwa.core.models.Config"), \
175
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
176
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
186
+ with (
187
+ patch("iwa.core.models.Config"),
188
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
189
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
190
+ ):
177
191
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
178
192
  sm = ServiceManager(mock_wallet)
179
193
  sm.service = Service(
@@ -199,9 +213,11 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
199
213
 
200
214
  mock_wallet.sign_and_send_transaction.return_value = (False, None)
201
215
 
202
- with patch("iwa.plugins.olas.service_manager.drain.StakingContract", return_value=mock_staking):
203
- success, amount = sm.claim_rewards()
204
- assert success is False
216
+ with patch(
217
+ "iwa.plugins.olas.service_manager.drain.StakingContract", return_value=mock_staking
218
+ ):
219
+ success, amount = sm.claim_rewards()
220
+ assert success is False
205
221
 
206
222
 
207
223
  # === SERVICE MANAGER SPIN_UP STATE TRANSITIONS (lines 1188-1241) ===
@@ -209,9 +225,11 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
209
225
 
210
226
  def test_sm_spin_up_state_mismatch_after_activation(mock_wallet):
211
227
  """Cover spin_up state mismatch after activation (lines 1188-1191)."""
212
- with patch("iwa.core.models.Config"), \
213
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
214
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
228
+ with (
229
+ patch("iwa.core.models.Config"),
230
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
231
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
232
+ ):
215
233
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
216
234
  sm = ServiceManager(mock_wallet)
217
235
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
@@ -233,9 +251,11 @@ def test_sm_spin_up_state_mismatch_after_activation(mock_wallet):
233
251
 
234
252
  def test_sm_spin_up_registration_fails(mock_wallet):
235
253
  """Cover spin_up registration failure (lines 1199-1201)."""
236
- with patch("iwa.core.models.Config"), \
237
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
238
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
254
+ with (
255
+ patch("iwa.core.models.Config"),
256
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
257
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
258
+ ):
239
259
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
240
260
  sm = ServiceManager(mock_wallet)
241
261
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
@@ -253,9 +273,11 @@ def test_sm_spin_up_registration_fails(mock_wallet):
253
273
 
254
274
  def test_sm_spin_up_deploy_fails(mock_wallet):
255
275
  """Cover spin_up deploy failure (lines 1216-1218)."""
256
- with patch("iwa.core.models.Config"), \
257
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
258
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
276
+ with (
277
+ patch("iwa.core.models.Config"),
278
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
279
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
280
+ ):
259
281
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
260
282
  sm = ServiceManager(mock_wallet)
261
283
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
@@ -273,9 +295,11 @@ def test_sm_spin_up_deploy_fails(mock_wallet):
273
295
 
274
296
  def test_sm_wind_down_terminate_fails(mock_wallet):
275
297
  """Cover wind_down terminate failure (lines 1299-1301)."""
276
- with patch("iwa.core.models.Config"), \
277
- patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
278
- patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
298
+ with (
299
+ patch("iwa.core.models.Config"),
300
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
301
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
302
+ ):
279
303
  mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
280
304
  sm = ServiceManager(mock_wallet)
281
305
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
@@ -293,8 +317,10 @@ def test_sm_wind_down_terminate_fails(mock_wallet):
293
317
 
294
318
  def test_sm_wind_down_unbond_fails(mock_wallet):
295
319
  """Cover wind_down unbond failure (lines 1315-1317)."""
296
- with patch("iwa.core.models.Config"), \
297
- patch("iwa.plugins.olas.service_manager.base.ContractCache"):
320
+ with (
321
+ patch("iwa.core.models.Config"),
322
+ patch("iwa.plugins.olas.service_manager.base.ContractCache"),
323
+ ):
298
324
  sm = ServiceManager(mock_wallet)
299
325
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
300
326
 
@@ -0,0 +1,61 @@
1
+ """Tool to drain specific accounts to a master address."""
2
+
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
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -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/tools/reset_env.py CHANGED
@@ -25,6 +25,7 @@ def _reset_tenderly(profile: int) -> None:
25
25
  env = {"PYTHONPATH": "src"}
26
26
  # Merge with current env to keep PATH etc.
27
27
  import os
28
+
28
29
  full_env = os.environ.copy()
29
30
  full_env.update(env)
30
31
 
@@ -110,7 +111,7 @@ def main():
110
111
  parser.add_argument(
111
112
  "--keep-data",
112
113
  action="store_true",
113
- help="Reset only Tenderly, keeping config.yaml and wallet.json intact."
114
+ help="Reset only Tenderly, keeping config.yaml and wallet.json intact.",
114
115
  )
115
116
  args = parser.parse_args()
116
117
 
@@ -1,4 +1,5 @@
1
1
  """Script to verify ChainlistRPC functionality."""
2
+
2
3
  from iwa.core.chainlist import ChainlistRPC
3
4
 
4
5
 
@@ -32,7 +33,10 @@ def main() -> None:
32
33
 
33
34
  print("\nTracking info for first 5 RPCs:")
34
35
  for node in all_rpcs[:5]:
35
- print(f" - {node.url}: Tracking={node.is_tracking} (Privacy={node.privacy}, Tracking={node.tracking})")
36
+ print(
37
+ f" - {node.url}: Tracking={node.is_tracking} (Privacy={node.privacy}, Tracking={node.tracking})"
38
+ )
39
+
36
40
 
37
41
  if __name__ == "__main__":
38
42
  main()
@@ -330,9 +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.get_erc20_balance_with_retry(
334
- address, token, chain_name
335
- )
333
+ val_token = self.wallet.balance_service.get_erc20_balance_eth(address, token, chain_name)
336
334
  val_token_str = f"{val_token:.4f}" if val_token is not None else "-"
337
335
 
338
336
  if val_token is not None:
@@ -442,7 +440,7 @@ class WalletsScreen(VerticalScroll):
442
440
  def handler(tag):
443
441
  if tag is not None:
444
442
  tag = tag or f"Account {len(self.wallet.key_storage.accounts) + 1}"
445
- self.wallet.key_storage.create_account(tag)
443
+ self.wallet.key_storage.generate_new_account(tag)
446
444
  self.notify(f"Created new EOA: {escape(tag)}")
447
445
  self.refresh_accounts()
448
446
 
@@ -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.create_account(req.tag)
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
@@ -41,7 +41,9 @@ def _determine_bond_amount(req: CreateServiceRequest) -> int:
41
41
 
42
42
  if req.token_address and req.staking_contract:
43
43
  # If a contract is specified, we MUST use its requirements
44
- staking_name = wallet.account_service.get_tag_by_address(req.staking_contract) or req.staking_contract
44
+ staking_name = (
45
+ wallet.account_service.get_tag_by_address(req.staking_contract) or req.staking_contract
46
+ )
45
47
  logger.info(f"Fetching requirements from {staking_name}...")
46
48
  staking_contract = StakingContract(req.staking_contract, req.chain)
47
49
  reqs = staking_contract.get_requirements()
@@ -73,6 +75,7 @@ def _determine_bond_amount(req: CreateServiceRequest) -> int:
73
75
  )
74
76
  return bond_amount
75
77
 
78
+
76
79
  @router.post(
77
80
  "/create",
78
81
  summary="Create Service",
@@ -81,7 +84,6 @@ def _determine_bond_amount(req: CreateServiceRequest) -> int:
81
84
  def create_service(req: CreateServiceRequest, auth: bool = Depends(verify_auth)):
82
85
  """Create a new Olas service using spin_up for seamless deployment."""
83
86
  try:
84
-
85
87
  from iwa.plugins.olas.contracts.staking import StakingContract
86
88
  from iwa.plugins.olas.service_manager import ServiceManager
87
89
 
@@ -196,7 +198,9 @@ def deploy_service(
196
198
  if staking_contract:
197
199
  try:
198
200
  staking_obj = StakingContract(staking_contract, service.chain_name)
199
- staking_name = wallet.account_service.get_tag_by_address(staking_contract) or staking_contract
201
+ staking_name = (
202
+ wallet.account_service.get_tag_by_address(staking_contract) or staking_contract
203
+ )
200
204
  logger.info(f"Will stake in {staking_name} after deployment")
201
205
  except Exception as e:
202
206
  logger.warning(f"Could not set up staking contract: {e}")
@@ -230,7 +234,6 @@ def deploy_service(
230
234
  raise HTTPException(status_code=400, detail=str(e)) from None
231
235
 
232
236
 
233
-
234
237
  def _resolve_service_accounts(service) -> dict:
235
238
  """Resolve basic accounts info including owner_signer if applicable."""
236
239
  accounts = {}
@@ -279,7 +282,9 @@ def _resolve_service_balances(service, chain: str) -> dict:
279
282
  if role == "owner" and stored and hasattr(stored, "signers") and stored.signers:
280
283
  signer_addr = stored.signers[0]
281
284
  s_native = wallet.get_native_balance_eth(signer_addr, chain)
282
- s_olas_wei = wallet.balance_service.get_erc20_balance_wei(signer_addr, "OLAS", chain)
285
+ s_olas_wei = wallet.balance_service.get_erc20_balance_wei(
286
+ signer_addr, "OLAS", chain
287
+ )
283
288
  s_olas = float(s_olas_wei) / 1e18 if s_olas_wei else 0
284
289
  s_stored = wallet.key_storage.find_stored_account(signer_addr)
285
290
  balances["owner_signer"] = {