iwa 0.0.61__py3-none-any.whl → 0.0.64__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.
@@ -23,7 +23,7 @@ iwa/core/utils.py,sha256=FTYpIdQ1wnugD4lYU4TQ7d7_TlDs4CTUIhEpHGEJph4,4281
23
23
  iwa/core/wallet.py,sha256=xSGFOK5Wzh-ctLGhBMK1BySlXN0Ircpztyk1an21QiQ,13129
24
24
  iwa/core/chain/__init__.py,sha256=XJMmn0ed-_aVkY2iEMKpuTxPgIKBd41dexSVmEZTa-o,1604
25
25
  iwa/core/chain/errors.py,sha256=9SEbhxZ-qASPkzt-DoI51qq0GRJVqRgqgL720gO7a64,1275
26
- iwa/core/chain/interface.py,sha256=4VA2qRUZ40J38l7edaeGTlOnwcXLxZxFPo1Flitzl3M,28270
26
+ iwa/core/chain/interface.py,sha256=lW8jmxQNVrUeoBUuYUx9X5kwUhr-Xw3MHS1RsGQeaRQ,29064
27
27
  iwa/core/chain/manager.py,sha256=XHwn7ciapFCZVk0rPSJopUqM5Wu3Kpp6XrenkgTE1HA,1397
28
28
  iwa/core/chain/models.py,sha256=WUhAighMKcFdbAUkPU_3dkGbWyAUpRJqXMHLcWFC1xg,5261
29
29
  iwa/core/chain/rate_limiter.py,sha256=Ps1MrR4HHtylxgUAawe6DoC9tuqKagjQdKulqcJD2gs,9093
@@ -41,7 +41,7 @@ iwa/core/services/account.py,sha256=0l14qD8_-ZbN_hQUNa7bRZt0tkceHPPc4GHmB8UKqy4,
41
41
  iwa/core/services/balance.py,sha256=MSCEzPRDPlHIjaWD1A2X2oIuiMz5MFJjD7sSHUxQ8OM,3324
42
42
  iwa/core/services/plugin.py,sha256=GNNlbtELyHl7MNVChrypF76GYphxXduxDog4kx1MLi8,3277
43
43
  iwa/core/services/safe.py,sha256=vqvpk7aIqHljaG1zYYpmKdW4mi5OVuoyXcpReISPYM0,15744
44
- iwa/core/services/safe_executor.py,sha256=e7M4Z0w00W5H88I1Yf2qQGP_orI5FDDVSzgilhJaeQo,13509
44
+ iwa/core/services/safe_executor.py,sha256=TqpDgtvh8d5cedYAKBj7s1SW7EnomTT9MW_GnYWMxDE,17157
45
45
  iwa/core/services/transaction.py,sha256=FrGRWn1xo5rbGIr2ToZ2kPzapr3zmWW38oycyB87TK8,19971
46
46
  iwa/core/services/transfer/__init__.py,sha256=ZJfshFxJRsp8rkOqfVvd1cqEzIJ9tqBJh8pc0l90GLk,5576
47
47
  iwa/core/services/transfer/base.py,sha256=sohz-Ss2i-pGYGl4x9bD93cnYKcSvsXaXyvyRawvgQs,9043
@@ -70,7 +70,7 @@ iwa/plugins/olas/constants.py,sha256=BbEDho_TAh10cCGsrlk2vP1OVrS_ZWBE_cAEITd_658
70
70
  iwa/plugins/olas/events.py,sha256=HHjYu4pN3tuZATIh8vGWWzDb7z9wuqhsaTqI3_4H0-I,6086
71
71
  iwa/plugins/olas/importer.py,sha256=5xTtlQe5hO5bUCOg1sRuFkN2UcohYdJEQwfMm2JckyI,42088
72
72
  iwa/plugins/olas/mech_reference.py,sha256=CaSCpQnQL4F7wOG6Ox6Zdoy-uNEQ78YBwVLILQZKL8Q,5782
73
- iwa/plugins/olas/models.py,sha256=uXih8UcmBYj6PlxqP-cnyAiazJezjShjkCaN1Kqlb2I,4921
73
+ iwa/plugins/olas/models.py,sha256=VtDjSyc63Yxs3aManmALrcf7asehdQ5f-5Y6MtAdWIk,4056
74
74
  iwa/plugins/olas/plugin.py,sha256=kz21CxIGQw3Is6HC2dvKvFRIm9m1FRKHO0YgUuDEplQ,15650
75
75
  iwa/plugins/olas/contracts/activity_checker.py,sha256=OXh0SFPGfcpeD665ay-I19LqcIx38qEz8o62dw0A9zE,5361
76
76
  iwa/plugins/olas/contracts/base.py,sha256=y73aQbDq6l4zUpz_eQAg4MsLkTAEqjjupXlcvxjfgCI,240
@@ -93,8 +93,8 @@ iwa/plugins/olas/scripts/test_full_mech_flow.py,sha256=Fqoq5bn7Z_3YyRrnuqNAZy9cw
93
93
  iwa/plugins/olas/scripts/test_simple_lifecycle.py,sha256=8T50tOZx3afeECSfCNAb0rAHNtYOsBaeXlMwKXElCk8,2099
94
94
  iwa/plugins/olas/service_manager/__init__.py,sha256=GXiThMEY3nPgHUl1i-DLrF4h96z9jPxxI8Jepo2E1PM,1926
95
95
  iwa/plugins/olas/service_manager/base.py,sha256=EBPg0ymqgtAb7ZvVSfTt31QYgv_6gp4UAc6je00NLAg,5009
96
- iwa/plugins/olas/service_manager/drain.py,sha256=Hoa8rTIDwp9xGf_0O-7Nvn58LMZ_8PTOTr2ETUY5TjY,12920
97
- iwa/plugins/olas/service_manager/lifecycle.py,sha256=JCLGN7RjJbbPXXHOqvD64OKj2wH2NVh7o3mCBqJQEAQ,50570
96
+ iwa/plugins/olas/service_manager/drain.py,sha256=oIBZON_ypshqaI2MZGKHW63dLEkkIcyAqS5lTZy0Qc4,13714
97
+ iwa/plugins/olas/service_manager/lifecycle.py,sha256=Jz2WTsBE_kGjFnsnpQlUBmMM2csOuqEfYdvj4cLrNuU,50586
98
98
  iwa/plugins/olas/service_manager/mech.py,sha256=NVzVbEmyOe3wK92VEzCCOSuy3HDkEP1MSoVt7Av8Psk,27949
99
99
  iwa/plugins/olas/service_manager/staking.py,sha256=kT9OOQ4fi3FrIJB2T2gsvmv7DBRD6pDxqcXXh2o6iwc,29600
100
100
  iwa/plugins/olas/tests/conftest.py,sha256=4vM7EI00SrTGyeP0hNzsGSQHEj2-iznVgzlNh2_OGfo,739
@@ -104,9 +104,9 @@ iwa/plugins/olas/tests/test_mech_contracts.py,sha256=wvxuigPafF-ySIHVBdWVei3AO41
104
104
  iwa/plugins/olas/tests/test_olas_archiving.py,sha256=rwyP-9eZ1cNvvV4h4bBOrs_8qV-du9yt-VNMuH_13nY,3443
105
105
  iwa/plugins/olas/tests/test_olas_contracts.py,sha256=B8X-5l1KfYMoZOiM94_rcNzbILLl78rqt_jhyxzAOqE,10835
106
106
  iwa/plugins/olas/tests/test_olas_integration.py,sha256=LGkdeso5lvi7_0GjlS9EFlSs6PEEn_b5aD2USmperDA,23086
107
- iwa/plugins/olas/tests/test_olas_models.py,sha256=5scX-wvRLGH3G44S2okq_tyQ9Rk7Pd0Ak1zNCZ2HtI4,4957
107
+ iwa/plugins/olas/tests/test_olas_models.py,sha256=LCtU01v2LhPgSjzMBGlhRQ9nBb7sgcd7n7F1z-T-IuA,4977
108
108
  iwa/plugins/olas/tests/test_olas_view.py,sha256=2SsQYayeV3rf_mAPVvt4vINcMysAXmICkkQe3MRn4K8,10662
109
- iwa/plugins/olas/tests/test_olas_view_actions.py,sha256=jAxr9bjFNAaxGf1btIrxdMaHgJ0PWX9aDwVU-oPGMpk,5109
109
+ iwa/plugins/olas/tests/test_olas_view_actions.py,sha256=67al8ffNWMETI-foq-FjsXqpjdEruuH1sRCsGo_x9GE,5113
110
110
  iwa/plugins/olas/tests/test_olas_view_modals.py,sha256=8j0PNFjKqFC5V1kBdVFWNLMvqGt49H6fLSYGxn02c8o,5562
111
111
  iwa/plugins/olas/tests/test_plugin.py,sha256=RVgU-Cq6t_3mOh90xFAGwlJOV7ZIgp0VNaK5ZAxisAQ,2565
112
112
  iwa/plugins/olas/tests/test_plugin_full.py,sha256=55EBa07JhJLVG3IMi6QKlR_ivWLYCdLQTySP66qbEXo,8584
@@ -115,9 +115,9 @@ iwa/plugins/olas/tests/test_service_manager.py,sha256=_mFRptssimITHhjvZA5jUPU2bI
115
115
  iwa/plugins/olas/tests/test_service_manager_errors.py,sha256=-qpLmU4Uiqqtre59L2wXpO4WPMs4ej_K_gAL3naEvRg,8554
116
116
  iwa/plugins/olas/tests/test_service_manager_flows.py,sha256=ZSmBJNa18d_MyAaLQRoPpfFYRwzmk9k-5AhSAGd7WeI,20737
117
117
  iwa/plugins/olas/tests/test_service_manager_mech.py,sha256=qG6qu5IPRNypXUsblU2OEkuiuwDJ0TH8RXZbibmTFcQ,4937
118
- iwa/plugins/olas/tests/test_service_manager_rewards.py,sha256=jVAe4HSAxDfnq8Ec-JMsvE2E0HYsydO78ITFHnSSka4,11993
118
+ iwa/plugins/olas/tests/test_service_manager_rewards.py,sha256=2YCrXBU5bEkPuhBoGBhjnO1nA2qwHxn5Ivrror18FHM,12248
119
119
  iwa/plugins/olas/tests/test_service_manager_validation.py,sha256=ajlfH5uc4mAHf8A7GLE5cW7X8utM2vUilM0JdGDdlVg,5382
120
- iwa/plugins/olas/tests/test_service_staking.py,sha256=ETNnWzV52FDhK0mrPb9xl9gSiYRH2EeDVxcaQIV0d5Q,15849
120
+ iwa/plugins/olas/tests/test_service_staking.py,sha256=78yyPoLo51N1aQyDxjzj7I0a263JHKHqekL-W3oQAsw,15914
121
121
  iwa/plugins/olas/tests/test_staking_integration.py,sha256=QCBQf6P2ZmmsEGt2k8W2r53lG2aVRuoMJE-aFxVDLss,9701
122
122
  iwa/plugins/olas/tests/test_staking_validation.py,sha256=uug64jFcXYJ3Nw_lNa3O4fnhNr5wAWHHIrchSbR2MVE,4020
123
123
  iwa/plugins/olas/tui/__init__.py,sha256=5ZRsbC7J3z1xfkZRiwr4bLEklf78rNVjdswe2p7SlS8,28
@@ -163,10 +163,10 @@ iwa/web/static/app.js,sha256=hBjUSivxf5Uyy2H6BR-rfdvF8e7qBoDPK23aY3dagNY,114418
163
163
  iwa/web/static/index.html,sha256=q7s7plnMbN1Nkzr5bRxZgvgOFerUChEGIZW7SpAVtPc,28514
164
164
  iwa/web/static/style.css,sha256=7i6T96pS7gXSLDZfyp_87gRlyB9rpsFWJEHJ-dRY1ug,24371
165
165
  iwa/web/tests/test_web_endpoints.py,sha256=vA25YghHNB23sbmhD4ciesn_f_okSq0tjlkrSiKZ0rs,24007
166
- iwa/web/tests/test_web_olas.py,sha256=0CVSsrncOeJ3x0ECV7mVLQV_CXZRrOqGiVjgLIi6hZ8,16308
166
+ iwa/web/tests/test_web_olas.py,sha256=GunKEAzcbzL7FoUGMtEl8wqiqwYwA5lB9sOhfCNj0TA,16312
167
167
  iwa/web/tests/test_web_swap.py,sha256=7A4gBJFL01kIXPtW1E1J17SCsVc_0DmUn-R8kKrnnVA,2974
168
168
  iwa/web/tests/test_web_swap_coverage.py,sha256=zGNrzlhZ_vWDCvWmLcoUwFgqxnrp_ACbo49AtWBS_Kw,5584
169
- iwa-0.0.61.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
169
+ iwa-0.0.64.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
170
170
  tests/legacy_cow.py,sha256=oOkZvIxL70ReEoD9oHQbOD5GpjIr6AGNHcOCgfPlerU,8389
171
171
  tests/legacy_safe.py,sha256=AssM2g13E74dNGODu_H0Q0y412lgqsrYnEzI97nm_Ts,2972
172
172
  tests/legacy_transaction_retry_logic.py,sha256=D9RqZ7DBu61Xr2djBAodU2p9UE939LL-DnQXswX5iQk,1497
@@ -178,11 +178,12 @@ tests/test_balance_service.py,sha256=wcuCOVszxPy8nPkldAVcEiygcOK3BuQt797fqAJvbp4
178
178
  tests/test_chain.py,sha256=VZoidSojWyt1y4mQdZdoZsjuuDZjLC6neTC-2SF_Q7I,13957
179
179
  tests/test_chain_interface.py,sha256=bgqGM8wJGZjc-BOX6i0K4sh06KCJl-6UAvrwl8x24lA,8324
180
180
  tests/test_chain_interface_coverage.py,sha256=fvrVvw8-DMwdsSFKQHUhpbfutrVRxnnTc-tjB7Bb-jo,3327
181
- tests/test_chainlist_enrichment.py,sha256=P7WctRhZ0sBTlsHsJXj5hv97CzgaEH556Nir2J8vWpg,12928
181
+ tests/test_chainlist_enrichment.py,sha256=hxfYjI54hT2pBXeacmQkLJGEPyuaAwDtNL5sEVZURp8,22065
182
182
  tests/test_cli.py,sha256=Pl4RC2xp1omiJUnL3Dza6pCmIoO29LJ0vGw33_ZpT5c,3980
183
183
  tests/test_contract.py,sha256=tApHAxsfKGawYJWA9PhTNrOZUE0VVAq79ruIe3KxeWY,14412
184
+ tests/test_contract_cache.py,sha256=TYIYPwIm_pVsdDjXJj_9-yDah_RFYB3PBn_fyE-a_YU,8454
184
185
  tests/test_db.py,sha256=dmbrupj0qlUeiiycZ2mzMFjf7HrDa6tcqMPY8zpiKIk,5710
185
- tests/test_drain_coverage.py,sha256=oFpY7tFPXWe9WLh3z2D4L2GxuAJO6_txLNxJmtgZT7M,7208
186
+ tests/test_drain_coverage.py,sha256=x-ANNt2YVJcuJApMe7VlzZE-1RRpSvGtDz5M_UXLc4I,18497
186
187
  tests/test_erc20.py,sha256=kNEw1afpm5EbXRNXkjpkBNZIy7Af1nqGlztKH5IWAwU,3074
187
188
  tests/test_gnosis_plugin.py,sha256=XMoHBCTrnVBq9bXYPzMUIrhr95caucMVRxooCjKrzjg,3454
188
189
  tests/test_keys.py,sha256=Qk4n3QDZ2HjXYRvehdrSlvDS_q3NLRLMnCq45Eo1Q9o,17551
@@ -203,14 +204,14 @@ tests/test_rpc_rate_limit.py,sha256=3P_Nd9voFmz-4r_Et-vw8W-Esbq5elSYmRBSOtJGx1Y,
203
204
  tests/test_rpc_rotation.py,sha256=a1cFKsf0fo-73_MSDnTuU6Zpv7bJHjrCVu3ANe8PXDU,12541
204
205
  tests/test_rpc_view.py,sha256=sgZ53KEHl8VGb7WKYa0VI7Cdxbf8JH1SdroHYbWHjfQ,2031
205
206
  tests/test_safe_coverage.py,sha256=KBxKz64XkK8CgN0N0LTNVKakf8Wg8EpghcBlLmDFmLs,6119
206
- tests/test_safe_executor.py,sha256=HYdyWdoKs3xK-1wJ9QGcLx58Dk2GV4-IpDqE9hUhA8I,22149
207
+ tests/test_safe_executor.py,sha256=xyiNmPo0Ux3jIcdz5AT98YSmha7Pn3HUDoBtBF6AYRE,25032
207
208
  tests/test_safe_integration.py,sha256=WWAKDio3N-CFyr5RRvphbOPdu3TI9WSM8IesfbFbvWQ,5363
208
209
  tests/test_safe_service.py,sha256=5ULlj0fPZRwg-4fCBJplhm4Msr_Beof7W-Zf_JljZc8,5782
209
210
  tests/test_service_manager_integration.py,sha256=I_BLUzEKrVTyg_8jqsUK0oFD3aQVPCRJ7z0gY8P-j04,2354
210
211
  tests/test_service_manager_structure.py,sha256=zK506ucCXCBHcjPYKrKEuK1bgq0xsbawyL8Y-wahXf8,868
211
212
  tests/test_service_transaction.py,sha256=IeqYhmRD-pIXffBJrBQwfPx-qnfNEJs0iPM3eCb8MLo,7054
212
213
  tests/test_staking_router.py,sha256=cnOtwWeQPu09kecVhlCf1WA4ONqs13OcQJhJCx2EOPY,3067
213
- tests/test_staking_simple.py,sha256=NHyZ1pcVQEJGFiGseC5m6Y9Y6FJGnRIFJUwhd1hAV9g,1138
214
+ tests/test_staking_simple.py,sha256=LN1ehVpNlT2oGGNhj0B1DAmazoL5xRmSCHB5AUL8900,19643
214
215
  tests/test_tables.py,sha256=1KQHgxuizoOrRxpubDdnzk9iaU5Lwyp3bcWP_hZD5uU,2686
215
216
  tests/test_transaction_service.py,sha256=q2IQ6cJ6sZtzc_pVCM_dv0vW7LW2sONNrK5Pvrm63rU,12816
216
217
  tests/test_transfer_multisend.py,sha256=PErjNqNwN66TMh4oVa307re64Ucccg1LkXqB0KlkmsI,6677
@@ -223,8 +224,8 @@ tests/test_utils.py,sha256=vkP49rYNI8BRzLpWR3WnKdDr8upeZjZcs7Rx0pjbQMo,1292
223
224
  tests/test_workers.py,sha256=MInwdkFY5LdmFB3o1odIaSD7AQZb3263hNafO1De5PE,2793
224
225
  tools/create_and_stake_service.py,sha256=1xwy_bJQI1j9yIQ968Oc9Db_F6mk1659LuuZntTASDE,3742
225
226
  tools/verify_drain.py,sha256=PkMjblyOOAuQge88FwfEzRtCYeEtJxXhPBmtQYCoQ-8,6743
226
- iwa-0.0.61.dist-info/METADATA,sha256=FLMOGU9L47jQQ45Cd7i64bfZNXCtNidn1ryV4-uBjCA,7337
227
- iwa-0.0.61.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
228
- iwa-0.0.61.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
229
- iwa-0.0.61.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
230
- iwa-0.0.61.dist-info/RECORD,,
227
+ iwa-0.0.64.dist-info/METADATA,sha256=9wa9UGdHrJz-WdMtiNRkwK9yvqAk-QDETX5UXJJaxWA,7337
228
+ iwa-0.0.64.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
229
+ iwa-0.0.64.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
230
+ iwa-0.0.64.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
231
+ iwa-0.0.64.dist-info/RECORD,,
@@ -343,12 +343,245 @@ class TestEnrichFromChainlist:
343
343
 
344
344
  chain = MagicMock(spec=SupportedChain)
345
345
  chain.name = "TestChain"
346
- chain.rpcs = [f"https://rpc{i}.example.com" for i in range(10)]
346
+ chain.rpcs = [f"https://rpc{i}.example.com" for i in range(20)]
347
347
  chain.rpc = "https://rpc0.example.com"
348
348
  chain.chain_id = 100
349
349
 
350
350
  with patch("iwa.core.chainlist.ChainlistRPC") as mock_cl_cls:
351
351
  ChainInterface(chain)
352
352
 
353
- # Already at MAX_RPCS=10, ChainlistRPC should not be called
353
+ # Already at MAX_RPCS=20, ChainlistRPC should not be called
354
354
  mock_cl_cls.assert_not_called()
355
+
356
+
357
+ class TestRPCNode:
358
+ """Test RPCNode dataclass."""
359
+
360
+ def test_is_tracking_privacy(self):
361
+ """Test is_tracking returns True for privacy tracking."""
362
+ node = RPCNode(url="https://example.com", is_working=True, privacy="privacy")
363
+ assert node.is_tracking is True
364
+
365
+ def test_is_tracking_limited(self):
366
+ """Test is_tracking returns True for limited tracking."""
367
+ node = RPCNode(url="https://example.com", is_working=True, tracking="limited")
368
+ assert node.is_tracking is True
369
+
370
+ def test_is_tracking_yes(self):
371
+ """Test is_tracking returns True for explicit yes tracking."""
372
+ node = RPCNode(url="https://example.com", is_working=True, tracking="yes")
373
+ assert node.is_tracking is True
374
+
375
+ def test_is_tracking_none(self):
376
+ """Test is_tracking returns False for no tracking."""
377
+ node = RPCNode(url="https://example.com", is_working=True, tracking="none")
378
+ assert node.is_tracking is False
379
+
380
+ def test_is_tracking_default(self):
381
+ """Test is_tracking returns False by default."""
382
+ node = RPCNode(url="https://example.com", is_working=True)
383
+ assert node.is_tracking is False
384
+
385
+
386
+ class TestFilterCandidates:
387
+ """Test _filter_candidates function."""
388
+
389
+ def test_max_candidates_limit(self):
390
+ """Test that _filter_candidates respects MAX_CHAINLIST_CANDIDATES."""
391
+ from iwa.core.chainlist import MAX_CHAINLIST_CANDIDATES, _filter_candidates
392
+
393
+ # Create more nodes than MAX_CHAINLIST_CANDIDATES
394
+ nodes = [
395
+ RPCNode(url=f"https://rpc{i}.example.com", is_working=True)
396
+ for i in range(MAX_CHAINLIST_CANDIDATES + 10)
397
+ ]
398
+
399
+ result = _filter_candidates(nodes, set())
400
+
401
+ # Should be limited to MAX_CHAINLIST_CANDIDATES
402
+ assert len(result) == MAX_CHAINLIST_CANDIDATES
403
+
404
+
405
+ class TestChainlistRPCFetchData:
406
+ """Test ChainlistRPC.fetch_data with caching."""
407
+
408
+ def test_fetch_data_uses_cache(self, tmp_path):
409
+ """Test fetch_data uses cached data when valid."""
410
+ import json
411
+ from unittest.mock import patch
412
+
413
+ cache_file = tmp_path / "chainlist_rpcs.json"
414
+ cache_data = [{"chainId": 100, "name": "Test", "rpc": []}]
415
+ cache_file.write_text(json.dumps(cache_data))
416
+
417
+ with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
418
+ with patch("iwa.core.chainlist.requests.Session") as mock_session:
419
+ cl = ChainlistRPC()
420
+ cl.fetch_data()
421
+
422
+ # Should not make network request when cache is valid
423
+ mock_session.return_value.get.assert_not_called()
424
+ assert cl._data == cache_data
425
+
426
+ def test_fetch_data_force_refresh(self, tmp_path):
427
+ """Test fetch_data ignores cache when force_refresh=True."""
428
+ import json
429
+
430
+ cache_file = tmp_path / "chainlist_rpcs.json"
431
+ cache_data = [{"chainId": 100, "name": "Cached", "rpc": []}]
432
+ cache_file.write_text(json.dumps(cache_data))
433
+
434
+ fresh_data = [{"chainId": 100, "name": "Fresh", "rpc": []}]
435
+
436
+ with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
437
+ with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
438
+ mock_session = MagicMock()
439
+ mock_session_cls.return_value.__enter__ = MagicMock(
440
+ return_value=mock_session
441
+ )
442
+ mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
443
+ mock_resp = MagicMock()
444
+ mock_resp.json.return_value = fresh_data
445
+ mock_session.get.return_value = mock_resp
446
+
447
+ cl = ChainlistRPC()
448
+ cl.fetch_data(force_refresh=True)
449
+
450
+ mock_session.get.assert_called_once()
451
+ assert cl._data == fresh_data
452
+
453
+ def test_fetch_data_network_error_falls_back_to_cache(self, tmp_path):
454
+ """Test fetch_data falls back to expired cache on network error."""
455
+ import json
456
+
457
+ cache_file = tmp_path / "chainlist_rpcs.json"
458
+ cache_data = [{"chainId": 100, "name": "ExpiredCache", "rpc": []}]
459
+ cache_file.write_text(json.dumps(cache_data))
460
+
461
+ with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
462
+ # Force cache to be expired by setting CACHE_TTL to 0
463
+ with patch.object(ChainlistRPC, "CACHE_TTL", 0):
464
+ with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
465
+ mock_session = MagicMock()
466
+ mock_session_cls.return_value.__enter__ = MagicMock(
467
+ return_value=mock_session
468
+ )
469
+ mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
470
+ mock_session.get.side_effect = requests.RequestException("Network error")
471
+
472
+ cl = ChainlistRPC()
473
+ cl.fetch_data()
474
+
475
+ # Should fall back to expired cache
476
+ assert cl._data == cache_data
477
+
478
+
479
+ class TestChainlistRPCGetRpcs:
480
+ """Test ChainlistRPC.get_rpcs and related methods."""
481
+
482
+ def test_get_chain_data_no_data(self):
483
+ """Test get_chain_data returns None when no data."""
484
+ with patch.object(ChainlistRPC, "fetch_data"):
485
+ cl = ChainlistRPC()
486
+ cl._data = []
487
+ result = cl.get_chain_data(999)
488
+ assert result is None
489
+
490
+ def test_get_chain_data_found(self):
491
+ """Test get_chain_data returns chain data when found."""
492
+ with patch.object(ChainlistRPC, "fetch_data"):
493
+ cl = ChainlistRPC()
494
+ cl._data = [{"chainId": 100, "name": "Gnosis"}, {"chainId": 1, "name": "Ethereum"}]
495
+ result = cl.get_chain_data(100)
496
+ assert result == {"chainId": 100, "name": "Gnosis"}
497
+
498
+ def test_get_rpcs_parses_nodes(self):
499
+ """Test get_rpcs parses RPC data into RPCNode objects."""
500
+ with patch.object(ChainlistRPC, "fetch_data"):
501
+ cl = ChainlistRPC()
502
+ cl._data = [
503
+ {
504
+ "chainId": 100,
505
+ "rpc": [
506
+ {"url": "https://rpc1.example.com", "privacy": "privacy"},
507
+ {"url": "https://rpc2.example.com", "tracking": "none"},
508
+ ],
509
+ }
510
+ ]
511
+ result = cl.get_rpcs(100)
512
+
513
+ assert len(result) == 2
514
+ assert result[0].url == "https://rpc1.example.com"
515
+ assert result[0].privacy == "privacy"
516
+ assert result[1].url == "https://rpc2.example.com"
517
+ assert result[1].tracking == "none"
518
+
519
+ def test_get_rpcs_chain_not_found(self):
520
+ """Test get_rpcs returns empty list when chain not found."""
521
+ with patch.object(ChainlistRPC, "fetch_data"):
522
+ cl = ChainlistRPC()
523
+ cl._data = [{"chainId": 1, "rpc": []}]
524
+ result = cl.get_rpcs(999)
525
+ assert result == []
526
+
527
+ def test_get_https_rpcs(self):
528
+ """Test get_https_rpcs filters to HTTPS/HTTP URLs."""
529
+ with patch.object(ChainlistRPC, "fetch_data"):
530
+ cl = ChainlistRPC()
531
+ cl._data = [
532
+ {
533
+ "chainId": 100,
534
+ "rpc": [
535
+ {"url": "https://rpc1.example.com"},
536
+ {"url": "http://rpc2.example.com"},
537
+ {"url": "wss://ws.example.com"},
538
+ ],
539
+ }
540
+ ]
541
+ result = cl.get_https_rpcs(100)
542
+
543
+ assert len(result) == 2
544
+ assert "https://rpc1.example.com" in result
545
+ assert "http://rpc2.example.com" in result
546
+ assert "wss://ws.example.com" not in result
547
+
548
+ def test_get_wss_rpcs(self):
549
+ """Test get_wss_rpcs filters to WSS/WS URLs."""
550
+ with patch.object(ChainlistRPC, "fetch_data"):
551
+ cl = ChainlistRPC()
552
+ cl._data = [
553
+ {
554
+ "chainId": 100,
555
+ "rpc": [
556
+ {"url": "https://rpc1.example.com"},
557
+ {"url": "wss://ws.example.com"},
558
+ {"url": "ws://ws2.example.com"},
559
+ ],
560
+ }
561
+ ]
562
+ result = cl.get_wss_rpcs(100)
563
+
564
+ assert len(result) == 2
565
+ assert "wss://ws.example.com" in result
566
+ assert "ws://ws2.example.com" in result
567
+ assert "https://rpc1.example.com" not in result
568
+
569
+
570
+ class TestGetValidatedRpcsEdgeCases:
571
+ """Test edge cases in get_validated_rpcs."""
572
+
573
+ def _make_node(self, url):
574
+ return RPCNode(url=url, is_working=True)
575
+
576
+ @patch.object(ChainlistRPC, "get_rpcs")
577
+ def test_returns_empty_when_all_filtered(self, mock_get_rpcs):
578
+ """Test returns empty list when all candidates are filtered."""
579
+ mock_get_rpcs.return_value = [
580
+ self._make_node("https://template.com/${API_KEY}"),
581
+ self._make_node("http://insecure.com"), # Not HTTPS
582
+ ]
583
+
584
+ cl = ChainlistRPC()
585
+ result = cl.get_validated_rpcs(100, existing_rpcs=[])
586
+
587
+ assert result == []
@@ -0,0 +1,253 @@
1
+ """Tests for contract instance caching."""
2
+
3
+ import time
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def reset_singleton():
11
+ """Reset the ContractCache singleton before each test."""
12
+ from iwa.core.contracts.cache import ContractCache
13
+
14
+ ContractCache._instance = None
15
+ yield
16
+ ContractCache._instance = None
17
+
18
+
19
+ class TestContractCache:
20
+ """Test ContractCache singleton and caching behavior."""
21
+
22
+ def test_singleton_pattern(self):
23
+ """Test that ContractCache is a singleton."""
24
+ from iwa.core.contracts.cache import ContractCache
25
+
26
+ c1 = ContractCache()
27
+ c2 = ContractCache()
28
+ assert c1 is c2
29
+
30
+ def test_get_contract_creates_new_instance(self):
31
+ """Test get_contract creates new instance when not cached."""
32
+ from iwa.core.contracts.cache import ContractCache
33
+
34
+ cache = ContractCache()
35
+ mock_cls = MagicMock(__name__="MockContract")
36
+ mock_instance = MagicMock()
37
+ mock_cls.return_value = mock_instance
38
+
39
+ result = cache.get_contract(
40
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
41
+ )
42
+
43
+ mock_cls.assert_called_once_with(
44
+ "0x1234567890123456789012345678901234567890", chain_name="gnosis"
45
+ )
46
+ assert result is mock_instance
47
+
48
+ def test_get_contract_returns_cached_instance(self):
49
+ """Test get_contract returns cached instance on second call."""
50
+ from iwa.core.contracts.cache import ContractCache
51
+
52
+ cache = ContractCache()
53
+ mock_cls = MagicMock(__name__="MockContract")
54
+ mock_instance = MagicMock()
55
+ mock_cls.return_value = mock_instance
56
+
57
+ result1 = cache.get_contract(
58
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
59
+ )
60
+ result2 = cache.get_contract(
61
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
62
+ )
63
+
64
+ # Should only create once
65
+ mock_cls.assert_called_once()
66
+ assert result1 is result2
67
+
68
+ def test_get_contract_raises_on_empty_address(self):
69
+ """Test get_contract raises ValueError for empty address."""
70
+ from iwa.core.contracts.cache import ContractCache
71
+
72
+ cache = ContractCache()
73
+ mock_cls = MagicMock(__name__="MockContract")
74
+
75
+ with pytest.raises(ValueError, match="Address is required"):
76
+ cache.get_contract(mock_cls, "", "gnosis")
77
+
78
+ def test_get_contract_respects_ttl_expiry(self):
79
+ """Test get_contract recreates instance after TTL expires."""
80
+ from iwa.core.contracts.cache import ContractCache
81
+
82
+ cache = ContractCache()
83
+ mock_cls = MagicMock(__name__="MockContract")
84
+ mock_cls.return_value = MagicMock()
85
+
86
+ # First call
87
+ cache.get_contract(
88
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis", ttl=0
89
+ )
90
+
91
+ # Wait for expiry (TTL=0 means immediate expiry)
92
+ time.sleep(0.01)
93
+
94
+ # Second call should create new instance
95
+ cache.get_contract(
96
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis", ttl=0
97
+ )
98
+
99
+ assert mock_cls.call_count == 2
100
+
101
+ def test_get_if_cached_returns_cached_instance(self):
102
+ """Test get_if_cached returns cached instance."""
103
+ from iwa.core.contracts.cache import ContractCache
104
+
105
+ cache = ContractCache()
106
+ mock_cls = MagicMock(__name__="MockContract")
107
+ mock_instance = MagicMock()
108
+ mock_cls.return_value = mock_instance
109
+
110
+ # First populate cache
111
+ cache.get_contract(
112
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
113
+ )
114
+
115
+ # get_if_cached should return it
116
+ result = cache.get_if_cached(
117
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
118
+ )
119
+
120
+ assert result is mock_instance
121
+
122
+ def test_get_if_cached_returns_none_when_not_cached(self):
123
+ """Test get_if_cached returns None when not cached."""
124
+ from iwa.core.contracts.cache import ContractCache
125
+
126
+ cache = ContractCache()
127
+ mock_cls = MagicMock(__name__="MockContract")
128
+
129
+ result = cache.get_if_cached(
130
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
131
+ )
132
+
133
+ assert result is None
134
+
135
+ def test_get_if_cached_returns_none_for_empty_address(self):
136
+ """Test get_if_cached returns None for empty address."""
137
+ from iwa.core.contracts.cache import ContractCache
138
+
139
+ cache = ContractCache()
140
+ mock_cls = MagicMock(__name__="MockContract")
141
+
142
+ result = cache.get_if_cached(mock_cls, "", "gnosis")
143
+
144
+ assert result is None
145
+
146
+ def test_get_if_cached_returns_none_after_expiry(self):
147
+ """Test get_if_cached returns None after TTL expires."""
148
+ from iwa.core.contracts.cache import ContractCache
149
+
150
+ cache = ContractCache()
151
+ cache.ttl = 0 # Immediate expiry
152
+ mock_cls = MagicMock(__name__="MockContract")
153
+ mock_cls.return_value = MagicMock()
154
+
155
+ # Populate cache
156
+ cache.get_contract(
157
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
158
+ )
159
+
160
+ time.sleep(0.01)
161
+
162
+ # get_if_cached should return None due to expiry
163
+ result = cache.get_if_cached(
164
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
165
+ )
166
+
167
+ assert result is None
168
+
169
+ def test_clear_removes_all_entries(self):
170
+ """Test clear removes all cached contracts."""
171
+ from iwa.core.contracts.cache import ContractCache
172
+
173
+ cache = ContractCache()
174
+ mock_cls = MagicMock(__name__="MockContract")
175
+ mock_cls.return_value = MagicMock()
176
+
177
+ # Populate cache
178
+ cache.get_contract(
179
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
180
+ )
181
+
182
+ cache.clear()
183
+
184
+ # get_if_cached should return None
185
+ result = cache.get_if_cached(
186
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
187
+ )
188
+ assert result is None
189
+
190
+ def test_invalidate_removes_specific_entry(self):
191
+ """Test invalidate removes specific cached contract."""
192
+ from iwa.core.contracts.cache import ContractCache
193
+
194
+ cache = ContractCache()
195
+ mock_cls1 = MagicMock(__name__="Contract1")
196
+ mock_cls2 = MagicMock(__name__="Contract2")
197
+ mock_cls1.return_value = MagicMock()
198
+ mock_cls2.return_value = MagicMock()
199
+
200
+ # Populate cache with two contracts
201
+ cache.get_contract(
202
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
203
+ )
204
+ cache.get_contract(
205
+ mock_cls2, "0xABCDEF1234567890123456789012345678901234", "gnosis"
206
+ )
207
+
208
+ # Invalidate only the first one
209
+ cache.invalidate(
210
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
211
+ )
212
+
213
+ # First should be gone
214
+ result1 = cache.get_if_cached(
215
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
216
+ )
217
+ assert result1 is None
218
+
219
+ # Second should still exist
220
+ result2 = cache.get_if_cached(
221
+ mock_cls2, "0xABCDEF1234567890123456789012345678901234", "gnosis"
222
+ )
223
+ assert result2 is not None
224
+
225
+ def test_invalidate_nonexistent_does_nothing(self):
226
+ """Test invalidate does nothing for non-existent entry."""
227
+ from iwa.core.contracts.cache import ContractCache
228
+
229
+ cache = ContractCache()
230
+ mock_cls = MagicMock(__name__="Contract")
231
+
232
+ # Should not raise
233
+ cache.invalidate(
234
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
235
+ )
236
+
237
+ def test_env_ttl_configuration(self):
238
+ """Test TTL is configurable via environment variable."""
239
+ from iwa.core.contracts.cache import ContractCache
240
+
241
+ with patch.dict("os.environ", {"IWA_CONTRACT_CACHE_TTL": "7200"}):
242
+ ContractCache._instance = None
243
+ cache = ContractCache()
244
+ assert cache.ttl == 7200
245
+
246
+ def test_invalid_env_ttl_uses_default(self):
247
+ """Test invalid TTL env var uses default value."""
248
+ from iwa.core.contracts.cache import ContractCache
249
+
250
+ with patch.dict("os.environ", {"IWA_CONTRACT_CACHE_TTL": "invalid"}):
251
+ ContractCache._instance = None
252
+ cache = ContractCache()
253
+ assert cache.ttl == 3600