iwa 0.0.19__py3-none-any.whl → 0.0.21__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 (39) hide show
  1. iwa/core/chain/interface.py +8 -1
  2. iwa/core/chainlist.py +116 -0
  3. iwa/core/constants.py +1 -0
  4. iwa/core/contracts/cache.py +131 -0
  5. iwa/core/contracts/contract.py +7 -0
  6. iwa/core/monitor.py +2 -2
  7. iwa/core/rpc_monitor.py +60 -0
  8. iwa/core/services/safe.py +2 -2
  9. iwa/plugins/gnosis/safe.py +1 -1
  10. iwa/plugins/gnosis/tests/test_safe.py +1 -1
  11. iwa/plugins/olas/contracts/activity_checker.py +63 -25
  12. iwa/plugins/olas/contracts/staking.py +115 -19
  13. iwa/plugins/olas/events.py +141 -0
  14. iwa/plugins/olas/plugin.py +2 -2
  15. iwa/plugins/olas/service_manager/base.py +7 -2
  16. iwa/plugins/olas/service_manager/lifecycle.py +30 -5
  17. iwa/plugins/olas/service_manager/mech.py +9 -0
  18. iwa/plugins/olas/service_manager/staking.py +6 -2
  19. iwa/plugins/olas/tests/test_olas_integration.py +38 -10
  20. iwa/plugins/olas/tests/test_plugin_full.py +3 -5
  21. iwa/plugins/olas/tests/test_service_manager.py +7 -1
  22. iwa/plugins/olas/tests/test_service_manager_errors.py +22 -11
  23. iwa/plugins/olas/tests/test_service_manager_flows.py +24 -8
  24. iwa/plugins/olas/tests/test_service_staking.py +59 -15
  25. iwa/plugins/olas/tests/test_staking_validation.py +8 -14
  26. iwa/tools/test_chainlist.py +38 -0
  27. iwa/tui/rpc.py +1 -1
  28. iwa/tui/screens/wallets.py +2 -2
  29. iwa/tui/tests/test_rpc.py +2 -2
  30. iwa/tui/widgets/base.py +1 -1
  31. iwa/web/routers/olas/staking.py +9 -4
  32. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/METADATA +1 -1
  33. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/RECORD +39 -33
  34. tests/test_monitor.py +3 -3
  35. tests/test_rpc_efficiency.py +103 -0
  36. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/WHEEL +0 -0
  37. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/entry_points.txt +0 -0
  38. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/licenses/LICENSE +0 -0
  39. {iwa-0.0.19.dist-info → iwa-0.0.21.dist-info}/top_level.txt +0 -0
@@ -23,12 +23,19 @@ def mock_wallet():
23
23
  return wallet
24
24
 
25
25
 
26
- def setup_manager(mock_wallet):
26
+ @pytest.fixture
27
+ def mock_manager(mock_wallet):
27
28
  """Setup a ServiceManager with mocked dependencies."""
28
- with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls:
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
+
33
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
34
+
29
35
  mock_cfg = mock_cfg_cls.return_value
30
36
  mock_cfg.plugins = {"olas": MagicMock()}
31
37
  mock_cfg.plugins["olas"].get_service.return_value = None
38
+
32
39
  with patch(
33
40
  "iwa.plugins.olas.service_manager.OLAS_CONTRACTS",
34
41
  {
@@ -43,18 +50,22 @@ def setup_manager(mock_wallet):
43
50
  mock_if = mock_if_cls.return_value
44
51
  mock_if.get.return_value.chain.name.lower.return_value = "gnosis"
45
52
  mock_if.get.return_value.get_contract_address.return_value = VALID_ADDR
53
+
46
54
  manager = ServiceManager(mock_wallet)
47
55
  manager.registry = MagicMock()
48
56
  manager.manager_contract = MagicMock()
49
57
  manager.olas_config = mock_cfg.plugins["olas"]
50
58
  manager.chain_name = "gnosis"
59
+ # Fix recursive mock issue by setting explicit return value
60
+ manager.registry.chain_interface = MagicMock()
51
61
  manager.registry.chain_interface.get_contract_address.return_value = VALID_ADDR
52
- return manager
62
+
63
+ yield manager
53
64
 
54
65
 
55
- def test_service_manager_mech_requests_failures(mock_wallet):
66
+ def test_service_manager_mech_requests_failures(mock_manager):
56
67
  """Test failure paths in mech requests."""
57
- manager = setup_manager(mock_wallet)
68
+ manager = mock_manager
58
69
 
59
70
  # Service missing
60
71
  manager.service = None
@@ -99,9 +110,9 @@ def test_service_manager_mech_requests_failures(mock_wallet):
99
110
  assert manager.send_mech_request(b"data", use_marketplace=False) is None
100
111
 
101
112
 
102
- def test_service_manager_lifecycle_failures(mock_wallet):
113
+ def test_service_manager_lifecycle_failures(mock_manager, mock_wallet):
103
114
  """Test failure paths in lifecycle methods."""
104
- manager = setup_manager(mock_wallet)
115
+ manager = mock_manager
105
116
  manager.service = Service(service_name="t", chain_name="gnosis", service_id=1, agent_ids=[1])
106
117
 
107
118
  # register_agent failures
@@ -147,9 +158,9 @@ def test_service_manager_lifecycle_failures(mock_wallet):
147
158
  assert manager.wind_down() is True
148
159
 
149
160
 
150
- def test_service_manager_staking_status_failures(mock_wallet):
161
+ def test_service_manager_staking_status_failures(mock_manager):
151
162
  """Test failure paths in get_staking_status."""
152
- manager = setup_manager(mock_wallet)
163
+ manager = mock_manager
153
164
 
154
165
  # Service missing
155
166
  manager.service = None
@@ -178,9 +189,9 @@ def test_service_manager_staking_status_failures(mock_wallet):
178
189
  assert status.mech_requests_this_epoch == 0
179
190
 
180
191
 
181
- def test_service_manager_verify_event_exception(mock_wallet):
192
+ def test_service_manager_verify_event_exception(mock_manager):
182
193
  """Test exception path in _execute_mech_tx."""
183
- manager = setup_manager(mock_wallet)
194
+ manager = mock_manager
184
195
  manager.service = Service(
185
196
  service_name="t",
186
197
  chain_name="gnosis",
@@ -45,10 +45,12 @@ def mock_config():
45
45
  @patch("iwa.plugins.olas.service_manager.base.Config")
46
46
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
47
47
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
48
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
48
49
  def test_create_service_success(
49
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
50
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
50
51
  ):
51
52
  """Test successful service creation."""
53
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
52
54
  # Setup Config with new OlasConfig structure
53
55
  mock_config_inst = mock_config_cls.return_value
54
56
  mock_olas_config = MagicMock()
@@ -84,10 +86,12 @@ def test_create_service_success(
84
86
  @patch("iwa.plugins.olas.service_manager.base.Config")
85
87
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
86
88
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
89
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
87
90
  def test_create_service_failures(
88
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
91
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
89
92
  ):
90
93
  """Test service creation failure modes."""
94
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
91
95
  mock_config_inst = mock_config_cls.return_value
92
96
  mock_olas_config = MagicMock()
93
97
  mock_olas_config.get_service.return_value = None
@@ -139,10 +143,12 @@ def test_create_service_failures(
139
143
  @patch("iwa.plugins.olas.service_manager.base.Config")
140
144
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
141
145
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
146
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
142
147
  def test_create_service_with_approval(
143
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
148
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
144
149
  ):
145
150
  """Test service creation with token approval."""
151
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
146
152
  mock_config_inst = mock_config_cls.return_value
147
153
  mock_olas_config = MagicMock()
148
154
  mock_olas_config.get_service.return_value = None
@@ -170,10 +176,12 @@ def test_create_service_with_approval(
170
176
  @patch("iwa.plugins.olas.service_manager.base.Config")
171
177
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
172
178
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
179
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
173
180
  def test_activate_registration(
174
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
181
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
175
182
  ):
176
183
  """Test service registration activation."""
184
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
177
185
  mock_config_inst = mock_config_cls.return_value
178
186
  mock_olas_config = MagicMock()
179
187
  mock_service = MagicMock()
@@ -228,8 +236,10 @@ def test_activate_registration(
228
236
  @patch("iwa.plugins.olas.service_manager.base.Config")
229
237
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
230
238
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
231
- def test_register_agent(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
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):
232
241
  """Test agent registration flow."""
242
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
233
243
  mock_config_inst = mock_config_cls.return_value
234
244
  mock_olas_config = MagicMock()
235
245
  mock_service = MagicMock()
@@ -277,8 +287,10 @@ def test_register_agent(mock_sm_contract, mock_registry_contract, mock_config_cl
277
287
  @patch("iwa.plugins.olas.service_manager.base.Config")
278
288
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
279
289
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
280
- def test_deploy(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
290
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
291
+ def test_deploy(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
281
292
  """Test service deployment."""
293
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
282
294
  # Setup mock service
283
295
  mock_service = MagicMock()
284
296
  mock_service.service_id = 123
@@ -341,8 +353,10 @@ def test_deploy(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_
341
353
  @patch("iwa.plugins.olas.service_manager.base.Config")
342
354
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
343
355
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
344
- def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
356
+ @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):
345
358
  """Test service termination."""
359
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
346
360
  # Setup mock service
347
361
  mock_service = MagicMock()
348
362
  mock_service.service_id = 123
@@ -401,8 +415,10 @@ def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mo
401
415
  @patch(
402
416
  "iwa.plugins.olas.service_manager.base.ServiceManagerContract"
403
417
  ) # MUST mock specifically here
404
- def test_stake(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
418
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
419
+ def test_stake(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
405
420
  """Test service staking."""
421
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
406
422
  # Setup mock service
407
423
  mock_service = MagicMock()
408
424
  mock_service.service_id = 123
@@ -33,7 +33,10 @@ 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"):
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):
39
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
37
40
  sm = ServiceManager(mock_wallet)
38
41
  sm.service = Service(
39
42
  service_name="t", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
@@ -48,7 +51,10 @@ def test_sm_unstake_not_staked(mock_wallet):
48
51
 
49
52
  def test_sm_unstake_tx_fails(mock_wallet):
50
53
  """Cover unstake transaction failure (lines 766-768)."""
51
- with patch("iwa.core.models.Config"):
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):
57
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
52
58
  sm = ServiceManager(mock_wallet)
53
59
  sm.service = Service(
54
60
  service_name="t", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
@@ -71,7 +77,10 @@ def test_sm_unstake_tx_fails(mock_wallet):
71
77
 
72
78
  def test_sm_get_staking_status_no_staking_address(mock_wallet):
73
79
  """Cover get_staking_status with no staking address (lines 831)."""
74
- with patch("iwa.core.models.Config"):
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):
83
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
75
84
  sm = ServiceManager(mock_wallet)
76
85
  sm.service = Service(
77
86
  service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
@@ -88,7 +97,10 @@ def test_sm_get_staking_status_no_staking_address(mock_wallet):
88
97
 
89
98
  def test_sm_get_staking_status_with_full_info(mock_wallet):
90
99
  """Cover get_staking_status with complete info (lines 866-891)."""
91
- with patch("iwa.core.models.Config"):
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):
103
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
92
104
  sm = ServiceManager(mock_wallet)
93
105
  sm.service = Service(
94
106
  service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
@@ -130,7 +142,10 @@ def test_sm_get_staking_status_with_full_info(mock_wallet):
130
142
 
131
143
  def test_sm_claim_rewards_no_service(mock_wallet):
132
144
  """Cover claim_rewards with no service (lines 936-938)."""
133
- with patch("iwa.core.models.Config"):
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):
148
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
134
149
  sm = ServiceManager(mock_wallet)
135
150
  sm.service = None
136
151
 
@@ -141,7 +156,10 @@ def test_sm_claim_rewards_no_service(mock_wallet):
141
156
 
142
157
  def test_sm_claim_rewards_no_staking_address(mock_wallet):
143
158
  """Cover claim_rewards with no staking address (lines 939-943)."""
144
- with patch("iwa.core.models.Config"):
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):
162
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
145
163
  sm = ServiceManager(mock_wallet)
146
164
  sm.service = Service(
147
165
  service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=None
@@ -153,7 +171,10 @@ def test_sm_claim_rewards_no_staking_address(mock_wallet):
153
171
 
154
172
  def test_sm_claim_rewards_tx_fails(mock_wallet):
155
173
  """Cover claim_rewards transaction failure (lines 967-968)."""
156
- with patch("iwa.core.models.Config"):
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):
177
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
157
178
  sm = ServiceManager(mock_wallet)
158
179
  sm.service = Service(
159
180
  service_name="t",
@@ -165,12 +186,22 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
165
186
 
166
187
  mock_staking = MagicMock()
167
188
  mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
189
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
190
+ mock_staking.get_accrued_rewards.return_value = 100
191
+
192
+ def get_contract_side_effect(cls, *args, **kwargs):
193
+ print(f"DEBUG: get_contract called with {cls!r}")
194
+ if "StakingContract" in str(cls):
195
+ return mock_staking
196
+ return cls(*args, **kwargs)
197
+
198
+ mock_cache.return_value.get_contract.side_effect = get_contract_side_effect
168
199
 
169
200
  mock_wallet.sign_and_send_transaction.return_value = (False, None)
170
201
 
171
- with patch("iwa.plugins.olas.service_manager.StakingContract", return_value=mock_staking):
172
- success, amount = sm.claim_rewards()
173
- assert success is False
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
174
205
 
175
206
 
176
207
  # === SERVICE MANAGER SPIN_UP STATE TRANSITIONS (lines 1188-1241) ===
@@ -178,7 +209,10 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
178
209
 
179
210
  def test_sm_spin_up_state_mismatch_after_activation(mock_wallet):
180
211
  """Cover spin_up state mismatch after activation (lines 1188-1191)."""
181
- with patch("iwa.core.models.Config"):
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):
215
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
182
216
  sm = ServiceManager(mock_wallet)
183
217
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
184
218
 
@@ -199,7 +233,10 @@ def test_sm_spin_up_state_mismatch_after_activation(mock_wallet):
199
233
 
200
234
  def test_sm_spin_up_registration_fails(mock_wallet):
201
235
  """Cover spin_up registration failure (lines 1199-1201)."""
202
- with patch("iwa.core.models.Config"):
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):
239
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
203
240
  sm = ServiceManager(mock_wallet)
204
241
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
205
242
 
@@ -216,7 +253,10 @@ def test_sm_spin_up_registration_fails(mock_wallet):
216
253
 
217
254
  def test_sm_spin_up_deploy_fails(mock_wallet):
218
255
  """Cover spin_up deploy failure (lines 1216-1218)."""
219
- with patch("iwa.core.models.Config"):
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):
259
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
220
260
  sm = ServiceManager(mock_wallet)
221
261
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
222
262
 
@@ -233,7 +273,10 @@ def test_sm_spin_up_deploy_fails(mock_wallet):
233
273
 
234
274
  def test_sm_wind_down_terminate_fails(mock_wallet):
235
275
  """Cover wind_down terminate failure (lines 1299-1301)."""
236
- with patch("iwa.core.models.Config"):
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):
279
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
237
280
  sm = ServiceManager(mock_wallet)
238
281
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
239
282
 
@@ -250,7 +293,8 @@ def test_sm_wind_down_terminate_fails(mock_wallet):
250
293
 
251
294
  def test_sm_wind_down_unbond_fails(mock_wallet):
252
295
  """Cover wind_down unbond failure (lines 1315-1317)."""
253
- with patch("iwa.core.models.Config"):
296
+ with patch("iwa.core.models.Config"), \
297
+ patch("iwa.plugins.olas.service_manager.base.ContractCache"):
254
298
  sm = ServiceManager(mock_wallet)
255
299
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
256
300
 
@@ -25,19 +25,12 @@ def mock_staking():
25
25
  def test_staking_get_service_info_nested_tuple(mock_staking):
26
26
  """Test get_service_info with nested tuple result from web3."""
27
27
  nested_result = (("0xMultisig", "0xOwner", (1, 2), 1000, 500, 0),)
28
- mock_staking.call.side_effect = [
29
- "0x1", # activityChecker
30
- 100, # availableRewards
31
- 200, # balance
32
- 3600, # livenessPeriod
33
- 10, # rewardsPerSecond
34
- 10, # maxNumServices
35
- 50, # minStakingDeposit
36
- 7200, # minStakingDuration
37
- "0xToken", # stakingToken
38
- nested_result, # getServiceInfo
39
- 2000, # getNextRewardCheckpointTimestamp
40
- ]
28
+ mock_staking.call.side_effect = (
29
+ ["0x1"] # activityChecker
30
+ + [nested_result] # getServiceInfo
31
+ + [2000] * 5 # getNextRewardCheckpointTimestamp and any others
32
+ )
33
+
41
34
  # Re-init to trigger calls
42
35
  with (
43
36
  patch("iwa.plugins.olas.contracts.staking.ActivityCheckerContract"),
@@ -45,7 +38,8 @@ def test_staking_get_service_info_nested_tuple(mock_staking):
45
38
  ):
46
39
  mock_ci.get_instance.return_value.web3.eth.contract.return_value = MagicMock()
47
40
  staking = StakingContract("0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB", "gnosis")
48
- staking.call = MagicMock(side_effect=[nested_result, 2000])
41
+ # Update call on the new instance
42
+ staking.call = MagicMock(side_effect=mock_staking.call.side_effect)
49
43
  staking.activity_checker.get_multisig_nonces.return_value = (1, 3)
50
44
  staking.get_required_requests = MagicMock(return_value=5)
51
45
 
@@ -0,0 +1,38 @@
1
+ """Script to verify ChainlistRPC functionality."""
2
+ from iwa.core.chainlist import ChainlistRPC
3
+
4
+
5
+ def main() -> None:
6
+ """Run the verification script."""
7
+ print("Initializing ChainlistRPC...")
8
+ chainlist = ChainlistRPC()
9
+
10
+ print("Fetching data...")
11
+ chainlist.fetch_data()
12
+
13
+ gnosis_chain_id = 100
14
+ print(f"\n--- Gnosis Chain (ID: {gnosis_chain_id}) ---")
15
+
16
+ all_rpcs = chainlist.get_rpcs(gnosis_chain_id)
17
+ print(f"Total RPCs found: {len(all_rpcs)}")
18
+
19
+ https_rpcs = chainlist.get_https_rpcs(gnosis_chain_id)
20
+ print(f"HTTPS RPCs ({len(https_rpcs)}):")
21
+ for url in https_rpcs[:5]:
22
+ print(f" - {url}")
23
+ if len(https_rpcs) > 5:
24
+ print(" ... and more")
25
+
26
+ wss_rpcs = chainlist.get_wss_rpcs(gnosis_chain_id)
27
+ print(f"WSS RPCs ({len(wss_rpcs)}):")
28
+ for url in wss_rpcs[:5]:
29
+ print(f" - {url}")
30
+ if len(wss_rpcs) > 5:
31
+ print(" ... and more")
32
+
33
+ print("\nTracking info for first 5 RPCs:")
34
+ for node in all_rpcs[:5]:
35
+ print(f" - {node.url}: Tracking={node.is_tracking} (Privacy={node.privacy}, Tracking={node.tracking})")
36
+
37
+ if __name__ == "__main__":
38
+ main()
iwa/tui/rpc.py CHANGED
@@ -38,7 +38,7 @@ class RPCView(Static):
38
38
  results.append((chain_name, "N/A", "Not Configured", "-"))
39
39
  continue
40
40
 
41
- rpc_url = interface.chain.rpc
41
+ rpc_url = interface.current_rpc
42
42
  if not rpc_url:
43
43
  results.append((chain_name, "None", "Missing URL", "-"))
44
44
  continue
@@ -381,7 +381,7 @@ class WalletsScreen(VerticalScroll):
381
381
  self.stop_monitor()
382
382
  addresses = [acc.address for acc in self.wallet.key_storage.accounts.values()]
383
383
  for chain_name, interface in ChainInterfaces().items():
384
- if interface.chain.rpc:
384
+ if interface.current_rpc:
385
385
  monitor = EventMonitor(addresses, self.monitor_callback, chain_name)
386
386
 
387
387
  # Worker wrapper
@@ -497,7 +497,7 @@ class WalletsScreen(VerticalScroll):
497
497
  """Handle blockchain selection changes."""
498
498
  if event.value and event.value != self.active_chain:
499
499
  interface = ChainInterfaces().get(event.value)
500
- if not interface or not interface.chain.rpc:
500
+ if not interface or not interface.current_rpc:
501
501
  self.notify(f"No RPC for {event.value}", severity="warning")
502
502
  event.control.value = self.active_chain
503
503
  return
iwa/tui/tests/test_rpc.py CHANGED
@@ -71,7 +71,7 @@ def test_check_rpcs_success(rpc_view, mock_chain_interfaces):
71
71
  """Test check_rpcs with successful connections."""
72
72
  # Setup mock chain interfaces
73
73
  mock_gnosis = MagicMock()
74
- mock_gnosis.chain.rpc = "http://gnosis"
74
+ mock_gnosis.current_rpc = "http://gnosis"
75
75
  mock_gnosis.web3.is_connected.return_value = True
76
76
 
77
77
  mock_chain_interfaces.get.side_effect = lambda name: mock_gnosis if name == "gnosis" else None
@@ -99,7 +99,7 @@ def test_check_rpcs_success(rpc_view, mock_chain_interfaces):
99
99
  def test_check_rpcs_error(rpc_view, mock_chain_interfaces):
100
100
  """Test check_rpcs with connection error."""
101
101
  mock_eth = MagicMock()
102
- mock_eth.chain.rpc = "http://eth"
102
+ mock_eth.current_rpc = "http://eth"
103
103
  mock_eth.web3.is_connected.side_effect = Exception("Connection fail")
104
104
 
105
105
  mock_chain_interfaces.get.side_effect = lambda name: mock_eth if name == "ethereum" else None
iwa/tui/widgets/base.py CHANGED
@@ -39,7 +39,7 @@ class ChainSelector(Horizontal):
39
39
 
40
40
  for name in chain_names:
41
41
  interface = ChainInterfaces().get(name)
42
- if interface.chain.rpc:
42
+ if interface.current_rpc:
43
43
  label = name.title()
44
44
  chain_options.append((label, name))
45
45
  else:
@@ -105,16 +105,21 @@ def _check_availability(name, address, interface):
105
105
  """Check availability of a single staking contract."""
106
106
  # Import at module level would cause circular import, but we can cache it
107
107
  # The import is cached by Python after first call so this is efficient
108
+ from iwa.core.contracts.cache import ContractCache
108
109
  from iwa.plugins.olas.contracts.staking import StakingContract
109
110
 
110
111
  try:
111
- contract = StakingContract(address, chain_name=interface.chain.name)
112
+ # Use ContractCache to benefit from shared instances and property caching
113
+ contract = ContractCache().get_contract(
114
+ StakingContract, address, chain_name=interface.chain.name
115
+ )
112
116
 
113
117
  # StakingContract uses .call() which handles with_retry and rotation
118
+ # Use properties instead of .call() to leverage caching
114
119
  service_ids = contract.call("getServiceIds")
115
- max_services = contract.call("maxNumServices")
116
- min_deposit = contract.call("minStakingDeposit")
117
- staking_token = contract.call("stakingToken")
120
+ max_services = contract.max_num_services
121
+ min_deposit = contract.min_staking_deposit
122
+ staking_token = contract.staking_token_address
118
123
  used = len(service_ids)
119
124
 
120
125
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.19
3
+ Version: 0.0.21
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