iwa 0.0.62__tar.gz → 0.0.64__tar.gz

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 (238) hide show
  1. {iwa-0.0.62/src/iwa.egg-info → iwa-0.0.64}/PKG-INFO +1 -1
  2. {iwa-0.0.62 → iwa-0.0.64}/pyproject.toml +2 -2
  3. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/models.py +5 -22
  4. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/drain.py +34 -12
  5. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/lifecycle.py +3 -3
  6. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_models.py +5 -5
  7. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +1 -1
  8. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +6 -1
  9. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_staking.py +1 -0
  10. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/tests/test_web_olas.py +1 -1
  11. {iwa-0.0.62 → iwa-0.0.64/src/iwa.egg-info}/PKG-INFO +1 -1
  12. {iwa-0.0.62 → iwa-0.0.64}/src/iwa.egg-info/SOURCES.txt +1 -0
  13. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_chainlist_enrichment.py +233 -0
  14. iwa-0.0.64/src/tests/test_contract_cache.py +253 -0
  15. iwa-0.0.64/src/tests/test_drain_coverage.py +442 -0
  16. iwa-0.0.64/src/tests/test_staking_simple.py +503 -0
  17. iwa-0.0.62/src/tests/test_drain_coverage.py +0 -180
  18. iwa-0.0.62/src/tests/test_staking_simple.py +0 -31
  19. {iwa-0.0.62 → iwa-0.0.64}/LICENSE +0 -0
  20. {iwa-0.0.62 → iwa-0.0.64}/README.md +0 -0
  21. {iwa-0.0.62 → iwa-0.0.64}/setup.cfg +0 -0
  22. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/__init__.py +0 -0
  23. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/__main__.py +0 -0
  24. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/__init__.py +0 -0
  25. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/__init__.py +0 -0
  26. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/errors.py +0 -0
  27. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/interface.py +0 -0
  28. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/manager.py +0 -0
  29. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/models.py +0 -0
  30. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chain/rate_limiter.py +0 -0
  31. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/chainlist.py +0 -0
  32. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/cli.py +0 -0
  33. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/constants.py +0 -0
  34. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/__init__.py +0 -0
  35. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/abis/erc20.json +0 -0
  36. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/abis/multisend.json +0 -0
  37. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
  38. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/cache.py +0 -0
  39. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/contract.py +0 -0
  40. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/decoder.py +0 -0
  41. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/erc20.py +0 -0
  42. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/contracts/multisend.py +0 -0
  43. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/db.py +0 -0
  44. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/http.py +0 -0
  45. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/ipfs.py +0 -0
  46. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/keys.py +0 -0
  47. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/mnemonic.py +0 -0
  48. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/models.py +0 -0
  49. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/monitor.py +0 -0
  50. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/plugins.py +0 -0
  51. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/pricing.py +0 -0
  52. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/rpc_monitor.py +0 -0
  53. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/secrets.py +0 -0
  54. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/__init__.py +0 -0
  55. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/account.py +0 -0
  56. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/balance.py +0 -0
  57. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/plugin.py +0 -0
  58. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/safe.py +0 -0
  59. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/safe_executor.py +0 -0
  60. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transaction.py +0 -0
  61. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/__init__.py +0 -0
  62. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/base.py +0 -0
  63. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/erc20.py +0 -0
  64. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/multisend.py +0 -0
  65. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/native.py +0 -0
  66. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/services/transfer/swap.py +0 -0
  67. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tables.py +0 -0
  68. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/test.py +0 -0
  69. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
  70. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tests/test_ipfs.py +0 -0
  71. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tests/test_pricing.py +0 -0
  72. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tests/test_regression_fixes.py +0 -0
  73. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/tests/test_wallet.py +0 -0
  74. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/types.py +0 -0
  75. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/ui.py +0 -0
  76. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/utils.py +0 -0
  77. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/core/wallet.py +0 -0
  78. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/__init__.py +0 -0
  79. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/__init__.py +0 -0
  80. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
  81. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
  82. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
  83. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/types.py +0 -0
  84. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
  85. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/plugin.py +0 -0
  86. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/safe.py +0 -0
  87. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
  88. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
  89. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/__init__.py +0 -0
  90. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/constants.py +0 -0
  91. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
  92. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
  93. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
  94. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
  95. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
  96. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
  97. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
  98. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
  99. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
  100. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
  101. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
  102. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/base.py +0 -0
  103. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech.py +0 -0
  104. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
  105. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
  106. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/service.py +0 -0
  107. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/staking.py +0 -0
  108. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/events.py +0 -0
  109. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/importer.py +0 -0
  110. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/mech_reference.py +0 -0
  111. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/plugin.py +0 -0
  112. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
  113. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
  114. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
  115. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/base.py +0 -0
  116. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
  117. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
  118. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/conftest.py +0 -0
  119. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
  120. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
  121. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
  122. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
  123. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
  124. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
  125. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
  126. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
  127. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
  128. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
  129. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
  130. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
  131. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
  132. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
  133. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
  134. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
  135. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
  136. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
  137. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tui/__init__.py +0 -0
  138. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
  139. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/__init__.py +0 -0
  140. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/check_profile.py +0 -0
  141. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/drain_accounts.py +0 -0
  142. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/list_contracts.py +0 -0
  143. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/release.py +0 -0
  144. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/reset_env.py +0 -0
  145. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/reset_tenderly.py +0 -0
  146. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/restore_backup.py +0 -0
  147. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/test_chainlist.py +0 -0
  148. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tools/wallet_check.py +0 -0
  149. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/__init__.py +0 -0
  150. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/app.py +0 -0
  151. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/modals/__init__.py +0 -0
  152. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/modals/base.py +0 -0
  153. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/rpc.py +0 -0
  154. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/screens/__init__.py +0 -0
  155. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/screens/wallets.py +0 -0
  156. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/tests/test_app.py +0 -0
  157. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/tests/test_rpc.py +0 -0
  158. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
  159. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/tests/test_widgets.py +0 -0
  160. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/widgets/__init__.py +0 -0
  161. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/widgets/base.py +0 -0
  162. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/tui/workers.py +0 -0
  163. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/dependencies.py +0 -0
  164. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/models.py +0 -0
  165. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/accounts.py +0 -0
  166. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/__init__.py +0 -0
  167. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/admin.py +0 -0
  168. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/funding.py +0 -0
  169. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/general.py +0 -0
  170. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/services.py +0 -0
  171. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/olas/staking.py +0 -0
  172. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/state.py +0 -0
  173. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/swap.py +0 -0
  174. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/routers/transactions.py +0 -0
  175. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/server.py +0 -0
  176. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/static/app.js +0 -0
  177. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/static/index.html +0 -0
  178. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/static/style.css +0 -0
  179. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/tests/test_web_endpoints.py +0 -0
  180. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/tests/test_web_swap.py +0 -0
  181. {iwa-0.0.62 → iwa-0.0.64}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
  182. {iwa-0.0.62 → iwa-0.0.64}/src/iwa.egg-info/dependency_links.txt +0 -0
  183. {iwa-0.0.62 → iwa-0.0.64}/src/iwa.egg-info/entry_points.txt +0 -0
  184. {iwa-0.0.62 → iwa-0.0.64}/src/iwa.egg-info/requires.txt +0 -0
  185. {iwa-0.0.62 → iwa-0.0.64}/src/iwa.egg-info/top_level.txt +0 -0
  186. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_cow.py +0 -0
  187. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_safe.py +0 -0
  188. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_transaction_retry_logic.py +0 -0
  189. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_tui.py +0 -0
  190. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_wallets_screen.py +0 -0
  191. {iwa-0.0.62 → iwa-0.0.64}/src/tests/legacy_web.py +0 -0
  192. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_account_service.py +0 -0
  193. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_balance_service.py +0 -0
  194. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_chain.py +0 -0
  195. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_chain_interface.py +0 -0
  196. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_chain_interface_coverage.py +0 -0
  197. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_cli.py +0 -0
  198. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_contract.py +0 -0
  199. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_db.py +0 -0
  200. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_erc20.py +0 -0
  201. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_gnosis_plugin.py +0 -0
  202. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_keys.py +0 -0
  203. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_legacy_wallet.py +0 -0
  204. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_main.py +0 -0
  205. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_migration.py +0 -0
  206. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_mnemonic.py +0 -0
  207. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_modals.py +0 -0
  208. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_models.py +0 -0
  209. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_monitor.py +0 -0
  210. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_multisend.py +0 -0
  211. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_plugin_service.py +0 -0
  212. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rate_limiter.py +0 -0
  213. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rate_limiter_retry.py +0 -0
  214. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_reset_tenderly.py +0 -0
  215. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rpc_efficiency.py +0 -0
  216. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rpc_rate_limit.py +0 -0
  217. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rpc_rotation.py +0 -0
  218. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_rpc_view.py +0 -0
  219. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_safe_coverage.py +0 -0
  220. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_safe_executor.py +0 -0
  221. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_safe_integration.py +0 -0
  222. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_safe_service.py +0 -0
  223. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_service_manager_integration.py +0 -0
  224. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_service_manager_structure.py +0 -0
  225. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_service_transaction.py +0 -0
  226. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_staking_router.py +0 -0
  227. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_tables.py +0 -0
  228. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transaction_service.py +0 -0
  229. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transfer_multisend.py +0 -0
  230. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transfer_native.py +0 -0
  231. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transfer_security.py +0 -0
  232. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transfer_structure.py +0 -0
  233. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_transfer_swap_unit.py +0 -0
  234. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_ui_coverage.py +0 -0
  235. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_utils.py +0 -0
  236. {iwa-0.0.62 → iwa-0.0.64}/src/tests/test_workers.py +0 -0
  237. {iwa-0.0.62 → iwa-0.0.64}/src/tools/create_and_stake_service.py +0 -0
  238. {iwa-0.0.62 → iwa-0.0.64}/src/tools/verify_drain.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.62
3
+ Version: 0.0.64
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "iwa"
3
- version = "0.0.62"
3
+ version = "0.0.64"
4
4
  description = "A secure, modular, and plugin-based framework for crypto agents and ops"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -72,7 +72,7 @@ where = ["src"]
72
72
 
73
73
  [tool.ruff]
74
74
  line-length = 100
75
- target-version = "0.0.62"
75
+ target-version = "0.0.64"
76
76
  fix = true
77
77
 
78
78
  [tool.ruff.lint]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, List, Optional
4
4
 
5
- from pydantic import BaseModel, Field, root_validator
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
  from iwa.core.models import EthereumAddress
8
8
 
@@ -15,37 +15,20 @@ class Service(BaseModel):
15
15
  service_id: int # Unique per chain
16
16
  agent_ids: List[int] = Field(default_factory=list) # List of agent type IDs
17
17
 
18
- # New explicit owner fields
18
+ # Owner fields:
19
+ # - service_owner_eoa_address: EOA that owns the service (or signs for the multisig)
20
+ # - service_owner_multisig_address: Safe multisig owner (optional, if service is owned by a Safe)
19
21
  service_owner_eoa_address: Optional[EthereumAddress] = None
20
22
  service_owner_multisig_address: Optional[EthereumAddress] = None
21
23
 
22
- # Deprecated fields (kept for migration, removed from physical model via aliasing/validation)
23
- # Actually, we keep it optional but not used, or use migration logic.
24
- # Let's remove it from fields and rely on before validator to map it to eoa.
25
-
26
24
  agent_address: Optional[EthereumAddress] = None
27
25
  multisig_address: Optional[EthereumAddress] = None
28
26
  staking_contract_address: Optional[EthereumAddress] = None
29
27
  token_address: Optional[EthereumAddress] = None
30
28
 
31
- @root_validator(pre=True)
32
- def migrate_owner_fields(cls, values): # noqa: N805
33
- """Migrate legacy service_owner_address to service_owner_eoa_address."""
34
- # Check for legacy 'service_owner_address'
35
- if "service_owner_address" in values and values["service_owner_address"]:
36
- legacy_addr = values["service_owner_address"]
37
-
38
- # If service_owner_eoa_address is missing, use legacy
39
- if "service_owner_eoa_address" not in values or not values["service_owner_eoa_address"]:
40
- values["service_owner_eoa_address"] = legacy_addr
41
-
42
- # Remove legacy field from values so it doesn't cause extra field errors if we removed it from model
43
- # Or if strict.
44
- return values
45
-
46
29
  @property
47
30
  def service_owner_address(self) -> Optional[EthereumAddress]:
48
- """Backward compatibility property: Returns effective owner (Safe if present, else EOA)."""
31
+ """Returns effective owner address (Safe multisig if present, else EOA)."""
49
32
  return self.service_owner_multisig_address or self.service_owner_eoa_address
50
33
 
51
34
  @property
@@ -12,7 +12,9 @@ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
12
12
  class DrainManagerMixin:
13
13
  """Mixin for draining and service token management."""
14
14
 
15
- def claim_rewards(self, staking_contract: Optional[StakingContract] = None) -> Tuple[bool, int]:
15
+ def claim_rewards( # noqa: C901
16
+ self, staking_contract: Optional[StakingContract] = None
17
+ ) -> Tuple[bool, int]:
16
18
  """Claim staking rewards for the active service.
17
19
 
18
20
  The claimed OLAS tokens will be sent to the service's multisig (Safe).
@@ -51,13 +53,19 @@ class DrainManagerMixin:
51
53
  logger.info("Service not staked, skipping claim")
52
54
  return False, 0
53
55
 
54
- # Check accrued rewards
55
- accrued_rewards = staking_contract.get_accrued_rewards(service_id)
56
- if accrued_rewards == 0:
57
- logger.info("No accrued rewards to claim")
56
+ # Check claimable rewards using calculate_staking_reward for accurate value
57
+ # (get_accrued_rewards returns stored value which may be outdated)
58
+ try:
59
+ claimable_rewards = staking_contract.calculate_staking_reward(service_id)
60
+ except Exception:
61
+ # Fallback to stored value if calculation fails
62
+ claimable_rewards = staking_contract.get_accrued_rewards(service_id)
63
+
64
+ if claimable_rewards == 0:
65
+ logger.info("No rewards to claim")
58
66
  return False, 0
59
67
 
60
- logger.info(f"Claiming {accrued_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
68
+ logger.info(f"Claiming ~{claimable_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
61
69
 
62
70
  # Use service owner which holds the reward rights (not necessarily master)
63
71
  owner_address = self.service.service_owner_address or self.wallet.master_account.address
@@ -83,11 +91,19 @@ class DrainManagerMixin:
83
91
  return False, 0
84
92
 
85
93
  events = staking_contract.extract_events(receipt)
86
- if "RewardClaimed" not in [event["name"] for event in events]:
87
- logger.warning("RewardClaimed event not found, but transaction succeeded")
88
94
 
89
- logger.info(f"Successfully claimed {accrued_rewards / 1e18:.4f} OLAS rewards")
90
- return True, accrued_rewards
95
+ # Extract actual claimed amount from RewardClaimed event
96
+ claimed_amount = claimable_rewards # Default to estimated
97
+ for event in events:
98
+ if event["name"] == "RewardClaimed":
99
+ # RewardClaimed event has 'amount' or 'reward' field
100
+ claimed_amount = event["args"].get("amount", event["args"].get("reward", claimed_amount))
101
+ break
102
+ else:
103
+ logger.warning("RewardClaimed event not found, using estimated amount")
104
+
105
+ logger.info(f"Successfully claimed {claimed_amount / 1e18:.4f} OLAS rewards")
106
+ return True, claimed_amount
91
107
 
92
108
  def withdraw_rewards(self) -> Tuple[bool, float]:
93
109
  """Withdraw OLAS from the service Safe to the configured withdrawal address.
@@ -132,8 +148,14 @@ class DrainManagerMixin:
132
148
  return False, 0
133
149
 
134
150
  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
151
+ withdrawal_tag = (
152
+ self.wallet.account_service.get_tag_by_address(withdrawal_address)
153
+ or withdrawal_address
154
+ )
155
+ multisig_tag = (
156
+ self.wallet.account_service.get_tag_by_address(multisig_address)
157
+ or multisig_address
158
+ )
137
159
 
138
160
  logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
139
161
 
@@ -183,7 +183,7 @@ class LifecycleManagerMixin:
183
183
  service_name=service_name,
184
184
  chain_name=chain_name,
185
185
  agent_id_values=agent_id_values,
186
- service_owner_address=service_owner_account.address,
186
+ service_owner_eoa_address=service_owner_account.address,
187
187
  token_address=token_address,
188
188
  )
189
189
 
@@ -263,7 +263,7 @@ class LifecycleManagerMixin:
263
263
  service_name: Optional[str],
264
264
  chain_name: str,
265
265
  agent_id_values: List[int],
266
- service_owner_address: str,
266
+ service_owner_eoa_address: str,
267
267
  token_address: Optional[str],
268
268
  ) -> None:
269
269
  """Create and save the new Service model."""
@@ -272,7 +272,7 @@ class LifecycleManagerMixin:
272
272
  chain_name=chain_name,
273
273
  service_id=service_id,
274
274
  agent_ids=agent_id_values,
275
- service_owner_address=service_owner_address,
275
+ service_owner_eoa_address=service_owner_eoa_address,
276
276
  token_address=token_address,
277
277
  )
278
278
 
@@ -14,7 +14,7 @@ class TestOlasConfig:
14
14
  chain_name="gnosis",
15
15
  service_id=456,
16
16
  agent_ids=[25],
17
- service_owner_address="0x1234567890123456789012345678901234567890",
17
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
18
18
  )
19
19
 
20
20
  config.add_service(service)
@@ -30,7 +30,7 @@ class TestOlasConfig:
30
30
  chain_name="gnosis",
31
31
  service_id=789,
32
32
  agent_ids=[25],
33
- service_owner_address="0x1234567890123456789012345678901234567890",
33
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
34
34
  )
35
35
  config.services["gnosis:789"] = service
36
36
 
@@ -53,7 +53,7 @@ class TestOlasConfig:
53
53
  chain_name="ethereum",
54
54
  service_id=200,
55
55
  agent_ids=[25],
56
- service_owner_address="0x1234567890123456789012345678901234567890",
56
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
57
57
  )
58
58
  config.services["ethereum:200"] = service
59
59
 
@@ -116,7 +116,7 @@ class TestService:
116
116
  chain_name="gnosis",
117
117
  service_id=123,
118
118
  agent_ids=[25],
119
- service_owner_address="0x1234567890123456789012345678901234567890",
119
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
120
120
  )
121
121
 
122
122
  assert service.key == "gnosis:123"
@@ -133,7 +133,7 @@ class TestService:
133
133
  chain_name="gnosis",
134
134
  service_id=456,
135
135
  agent_ids=[25],
136
- service_owner_address="0x1234567890123456789012345678901234567890",
136
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
137
137
  staking_contract_address=staking_addr,
138
138
  token_address=token_addr,
139
139
  )
@@ -35,7 +35,7 @@ def mock_olas_config():
35
35
  chain_name="gnosis",
36
36
  agent_address=VALID_ADDR_1,
37
37
  multisig_address=VALID_ADDR_2,
38
- service_owner_address=VALID_ADDR_3,
38
+ service_owner_eoa_address=VALID_ADDR_3,
39
39
  staking_contract_address=VALID_ADDR_1,
40
40
  )
41
41
  config = OlasConfig(services={"gnosis:1": service})
@@ -108,6 +108,7 @@ def test_claim_rewards_no_accrued_rewards(mock_wallet):
108
108
 
109
109
  mock_staking = MagicMock()
110
110
  mock_staking.get_staking_state.return_value = StakingState.STAKED
111
+ mock_staking.calculate_staking_reward.return_value = 0
111
112
  mock_staking.get_accrued_rewards.return_value = 0
112
113
 
113
114
  success, amount = manager.claim_rewards(staking_contract=mock_staking)
@@ -129,9 +130,12 @@ def test_claim_rewards_success(mock_wallet):
129
130
 
130
131
  mock_staking = MagicMock()
131
132
  mock_staking.get_staking_state.return_value = StakingState.STAKED
133
+ mock_staking.calculate_staking_reward.return_value = 10 * 10**18 # 10 OLAS
132
134
  mock_staking.get_accrued_rewards.return_value = 10 * 10**18 # 10 OLAS
133
135
  mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
134
- mock_staking.extract_events.return_value = [{"name": "RewardClaimed"}]
136
+ mock_staking.extract_events.return_value = [
137
+ {"name": "RewardClaimed", "args": {"amount": 10 * 10**18}}
138
+ ]
135
139
 
136
140
  success, amount = manager.claim_rewards(staking_contract=mock_staking)
137
141
 
@@ -152,6 +156,7 @@ def test_claim_rewards_tx_fails(mock_wallet):
152
156
 
153
157
  mock_staking = MagicMock()
154
158
  mock_staking.get_staking_state.return_value = StakingState.STAKED
159
+ mock_staking.calculate_staking_reward.return_value = 10 * 10**18
155
160
  mock_staking.get_accrued_rewards.return_value = 10 * 10**18
156
161
  mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
157
162
  mock_wallet.sign_and_send_transaction.return_value = (False, {})
@@ -201,6 +201,7 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
201
201
  mock_staking = MagicMock()
202
202
  mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
203
203
  mock_staking.get_staking_state.return_value = StakingState.STAKED
204
+ mock_staking.calculate_staking_reward.return_value = 100
204
205
  mock_staking.get_accrued_rewards.return_value = 100
205
206
 
206
207
  def get_contract_side_effect(cls, *args, **kwargs):
@@ -43,7 +43,7 @@ def mock_olas_config():
43
43
  chain_name="gnosis",
44
44
  agent_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
45
45
  multisig_address="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
46
- service_owner_address="0x1111111111111111111111111111111111111111",
46
+ service_owner_eoa_address="0x1111111111111111111111111111111111111111",
47
47
  staking_contract_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
48
48
  )
49
49
  return OlasConfig(services={"gnosis:1": service})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.62
3
+ Version: 0.0.64
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
@@ -189,6 +189,7 @@ src/tests/test_chain_interface_coverage.py
189
189
  src/tests/test_chainlist_enrichment.py
190
190
  src/tests/test_cli.py
191
191
  src/tests/test_contract.py
192
+ src/tests/test_contract_cache.py
192
193
  src/tests/test_db.py
193
194
  src/tests/test_drain_coverage.py
194
195
  src/tests/test_erc20.py
@@ -352,3 +352,236 @@ class TestEnrichFromChainlist:
352
352
 
353
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 == []