iwa 0.0.60__tar.gz → 0.0.62__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 (237) hide show
  1. {iwa-0.0.60/src/iwa.egg-info → iwa-0.0.62}/PKG-INFO +1 -1
  2. {iwa-0.0.60 → iwa-0.0.62}/pyproject.toml +2 -2
  3. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/interface.py +87 -12
  4. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/manager.py +8 -0
  5. iwa-0.0.62/src/iwa/core/chainlist.py +299 -0
  6. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/safe_executor.py +110 -26
  7. {iwa-0.0.60 → iwa-0.0.62/src/iwa.egg-info}/PKG-INFO +1 -1
  8. {iwa-0.0.60 → iwa-0.0.62}/src/iwa.egg-info/SOURCES.txt +1 -0
  9. iwa-0.0.62/src/tests/test_chainlist_enrichment.py +354 -0
  10. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_safe_executor.py +278 -0
  11. iwa-0.0.62/src/tests/test_transaction_service.py +355 -0
  12. iwa-0.0.60/src/iwa/core/chainlist.py +0 -121
  13. iwa-0.0.60/src/tests/test_transaction_service.py +0 -179
  14. {iwa-0.0.60 → iwa-0.0.62}/LICENSE +0 -0
  15. {iwa-0.0.60 → iwa-0.0.62}/README.md +0 -0
  16. {iwa-0.0.60 → iwa-0.0.62}/setup.cfg +0 -0
  17. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/__init__.py +0 -0
  18. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/__main__.py +0 -0
  19. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/__init__.py +0 -0
  20. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/__init__.py +0 -0
  21. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/errors.py +0 -0
  22. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/models.py +0 -0
  23. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/chain/rate_limiter.py +0 -0
  24. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/cli.py +0 -0
  25. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/constants.py +0 -0
  26. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/__init__.py +0 -0
  27. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/abis/erc20.json +0 -0
  28. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/abis/multisend.json +0 -0
  29. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
  30. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/cache.py +0 -0
  31. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/contract.py +0 -0
  32. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/decoder.py +0 -0
  33. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/erc20.py +0 -0
  34. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/contracts/multisend.py +0 -0
  35. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/db.py +0 -0
  36. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/http.py +0 -0
  37. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/ipfs.py +0 -0
  38. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/keys.py +0 -0
  39. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/mnemonic.py +0 -0
  40. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/models.py +0 -0
  41. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/monitor.py +0 -0
  42. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/plugins.py +0 -0
  43. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/pricing.py +0 -0
  44. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/rpc_monitor.py +0 -0
  45. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/secrets.py +0 -0
  46. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/__init__.py +0 -0
  47. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/account.py +0 -0
  48. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/balance.py +0 -0
  49. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/plugin.py +0 -0
  50. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/safe.py +0 -0
  51. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transaction.py +0 -0
  52. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/__init__.py +0 -0
  53. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/base.py +0 -0
  54. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/erc20.py +0 -0
  55. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/multisend.py +0 -0
  56. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/native.py +0 -0
  57. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/services/transfer/swap.py +0 -0
  58. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tables.py +0 -0
  59. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/test.py +0 -0
  60. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
  61. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tests/test_ipfs.py +0 -0
  62. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tests/test_pricing.py +0 -0
  63. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tests/test_regression_fixes.py +0 -0
  64. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/tests/test_wallet.py +0 -0
  65. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/types.py +0 -0
  66. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/ui.py +0 -0
  67. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/utils.py +0 -0
  68. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/core/wallet.py +0 -0
  69. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/__init__.py +0 -0
  70. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/__init__.py +0 -0
  71. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
  72. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
  73. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
  74. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/cow/types.py +0 -0
  75. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
  76. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/plugin.py +0 -0
  77. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/safe.py +0 -0
  78. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
  79. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
  80. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/__init__.py +0 -0
  81. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/constants.py +0 -0
  82. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
  83. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
  84. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
  85. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
  86. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
  87. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
  88. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
  89. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
  90. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
  91. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
  92. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
  93. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/base.py +0 -0
  94. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/mech.py +0 -0
  95. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
  96. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
  97. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/service.py +0 -0
  98. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/contracts/staking.py +0 -0
  99. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/events.py +0 -0
  100. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/importer.py +0 -0
  101. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/mech_reference.py +0 -0
  102. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/models.py +0 -0
  103. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/plugin.py +0 -0
  104. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
  105. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
  106. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
  107. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/base.py +0 -0
  108. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
  109. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
  110. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
  111. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
  112. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/conftest.py +0 -0
  113. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
  114. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
  115. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
  116. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
  117. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
  118. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
  119. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
  120. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
  121. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
  122. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
  123. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
  124. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
  125. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
  126. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
  127. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
  128. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
  129. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
  130. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
  131. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
  132. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
  133. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
  134. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
  135. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tui/__init__.py +0 -0
  136. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
  137. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/__init__.py +0 -0
  138. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/check_profile.py +0 -0
  139. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/drain_accounts.py +0 -0
  140. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/list_contracts.py +0 -0
  141. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/release.py +0 -0
  142. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/reset_env.py +0 -0
  143. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/reset_tenderly.py +0 -0
  144. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/restore_backup.py +0 -0
  145. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/test_chainlist.py +0 -0
  146. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tools/wallet_check.py +0 -0
  147. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/__init__.py +0 -0
  148. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/app.py +0 -0
  149. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/modals/__init__.py +0 -0
  150. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/modals/base.py +0 -0
  151. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/rpc.py +0 -0
  152. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/screens/__init__.py +0 -0
  153. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/screens/wallets.py +0 -0
  154. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/tests/test_app.py +0 -0
  155. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/tests/test_rpc.py +0 -0
  156. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
  157. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/tests/test_widgets.py +0 -0
  158. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/widgets/__init__.py +0 -0
  159. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/widgets/base.py +0 -0
  160. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/tui/workers.py +0 -0
  161. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/dependencies.py +0 -0
  162. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/models.py +0 -0
  163. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/accounts.py +0 -0
  164. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/__init__.py +0 -0
  165. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/admin.py +0 -0
  166. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/funding.py +0 -0
  167. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/general.py +0 -0
  168. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/services.py +0 -0
  169. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/olas/staking.py +0 -0
  170. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/state.py +0 -0
  171. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/swap.py +0 -0
  172. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/routers/transactions.py +0 -0
  173. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/server.py +0 -0
  174. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/static/app.js +0 -0
  175. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/static/index.html +0 -0
  176. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/static/style.css +0 -0
  177. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/tests/test_web_endpoints.py +0 -0
  178. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/tests/test_web_olas.py +0 -0
  179. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/tests/test_web_swap.py +0 -0
  180. {iwa-0.0.60 → iwa-0.0.62}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
  181. {iwa-0.0.60 → iwa-0.0.62}/src/iwa.egg-info/dependency_links.txt +0 -0
  182. {iwa-0.0.60 → iwa-0.0.62}/src/iwa.egg-info/entry_points.txt +0 -0
  183. {iwa-0.0.60 → iwa-0.0.62}/src/iwa.egg-info/requires.txt +0 -0
  184. {iwa-0.0.60 → iwa-0.0.62}/src/iwa.egg-info/top_level.txt +0 -0
  185. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_cow.py +0 -0
  186. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_safe.py +0 -0
  187. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_transaction_retry_logic.py +0 -0
  188. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_tui.py +0 -0
  189. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_wallets_screen.py +0 -0
  190. {iwa-0.0.60 → iwa-0.0.62}/src/tests/legacy_web.py +0 -0
  191. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_account_service.py +0 -0
  192. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_balance_service.py +0 -0
  193. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_chain.py +0 -0
  194. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_chain_interface.py +0 -0
  195. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_chain_interface_coverage.py +0 -0
  196. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_cli.py +0 -0
  197. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_contract.py +0 -0
  198. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_db.py +0 -0
  199. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_drain_coverage.py +0 -0
  200. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_erc20.py +0 -0
  201. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_gnosis_plugin.py +0 -0
  202. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_keys.py +0 -0
  203. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_legacy_wallet.py +0 -0
  204. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_main.py +0 -0
  205. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_migration.py +0 -0
  206. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_mnemonic.py +0 -0
  207. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_modals.py +0 -0
  208. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_models.py +0 -0
  209. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_monitor.py +0 -0
  210. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_multisend.py +0 -0
  211. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_plugin_service.py +0 -0
  212. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rate_limiter.py +0 -0
  213. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rate_limiter_retry.py +0 -0
  214. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_reset_tenderly.py +0 -0
  215. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rpc_efficiency.py +0 -0
  216. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rpc_rate_limit.py +0 -0
  217. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rpc_rotation.py +0 -0
  218. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_rpc_view.py +0 -0
  219. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_safe_coverage.py +0 -0
  220. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_safe_integration.py +0 -0
  221. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_safe_service.py +0 -0
  222. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_service_manager_integration.py +0 -0
  223. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_service_manager_structure.py +0 -0
  224. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_service_transaction.py +0 -0
  225. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_staking_router.py +0 -0
  226. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_staking_simple.py +0 -0
  227. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_tables.py +0 -0
  228. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_transfer_multisend.py +0 -0
  229. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_transfer_native.py +0 -0
  230. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_transfer_security.py +0 -0
  231. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_transfer_structure.py +0 -0
  232. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_transfer_swap_unit.py +0 -0
  233. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_ui_coverage.py +0 -0
  234. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_utils.py +0 -0
  235. {iwa-0.0.60 → iwa-0.0.62}/src/tests/test_workers.py +0 -0
  236. {iwa-0.0.60 → iwa-0.0.62}/src/tools/create_and_stake_service.py +0 -0
  237. {iwa-0.0.60 → iwa-0.0.62}/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.60
3
+ Version: 0.0.62
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.60"
3
+ version = "0.0.62"
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.60"
75
+ target-version = "0.0.62"
76
76
  fix = true
77
77
 
78
78
  [tool.ruff.lint]
@@ -54,9 +54,41 @@ class ChainInterface:
54
54
 
55
55
  self._initial_block = 0
56
56
  self._rotation_lock = threading.Lock()
57
- self._session = requests.Session()
57
+ self._session = self._create_session()
58
+
59
+ # Enrich with public RPCs from ChainList (skip for Tenderly vNets)
60
+ if not self.is_tenderly:
61
+ self._enrich_rpcs_from_chainlist()
62
+
58
63
  self._init_web3()
59
64
 
65
+ def _create_session(self) -> requests.Session:
66
+ """Create a requests Session with bounded connection pooling.
67
+
68
+ Configures the session with limited pool sizes to prevent file
69
+ descriptor exhaustion during RPC rotations. Connections are reused
70
+ within the pool but won't accumulate unboundedly.
71
+ """
72
+ session = requests.Session()
73
+ # Limit pool size: we only talk to one RPC at a time, but may rotate
74
+ # through multiple during the session lifetime. Keep modest limits.
75
+ adapter = requests.adapters.HTTPAdapter(
76
+ pool_connections=5, # Max different hosts to keep connections to
77
+ pool_maxsize=10, # Max connections per host
78
+ )
79
+ session.mount("https://", adapter)
80
+ session.mount("http://", adapter)
81
+ return session
82
+
83
+ def close(self) -> None:
84
+ """Close the session and release all connections.
85
+
86
+ Call this when the ChainInterface is no longer needed to ensure
87
+ proper cleanup of network resources.
88
+ """
89
+ if hasattr(self, "_session") and self._session:
90
+ self._session.close()
91
+
60
92
  @property
61
93
  def current_rpc(self) -> str:
62
94
  """Get the current active RPC URL."""
@@ -251,6 +283,39 @@ class ChainInterface:
251
283
  ]
252
284
  return any(signal in err_text for signal in quota_signals)
253
285
 
286
+ # -- ChainList enrichment ----------------------------------------------
287
+
288
+ MAX_RPCS = 20 # Cap total RPCs per chain
289
+
290
+ def _enrich_rpcs_from_chainlist(self) -> None:
291
+ """Add validated public RPCs from ChainList to the rotation pool."""
292
+ if len(self.chain.rpcs) >= self.MAX_RPCS:
293
+ logger.debug(
294
+ f"{self.chain.name}: skipping ChainList enrichment "
295
+ f"(already have {len(self.chain.rpcs)} RPCs)"
296
+ )
297
+ return
298
+
299
+ try:
300
+ from iwa.core.chainlist import ChainlistRPC
301
+
302
+ chainlist = ChainlistRPC()
303
+ extra = chainlist.get_validated_rpcs(
304
+ self.chain.chain_id,
305
+ existing_rpcs=self.chain.rpcs,
306
+ max_results=self.MAX_RPCS - len(self.chain.rpcs),
307
+ )
308
+ if extra:
309
+ self.chain.rpcs.extend(extra)
310
+ logger.info(
311
+ f"Enriched {self.chain.name} with {len(extra)} "
312
+ f"ChainList RPCs (total: {len(self.chain.rpcs)})"
313
+ )
314
+ except Exception as e:
315
+ logger.debug(
316
+ f"ChainList enrichment failed for {self.chain.name}: {e}"
317
+ )
318
+
254
319
  # -- Per-RPC health tracking ------------------------------------------
255
320
 
256
321
  def _mark_rpc_backoff(self, index: int, seconds: float) -> None:
@@ -293,35 +358,45 @@ class ChainInterface:
293
358
 
294
359
  if should_rotate:
295
360
  failed_index = self._current_rpc_index
361
+ failed_rpc = sanitize_rpc_url(self.chain.rpcs[failed_index]) if self.chain.rpcs else "?"
296
362
 
297
363
  # Apply per-RPC backoff so smart rotation skips this RPC.
298
364
  if result["is_quota_exceeded"]:
299
- error_type = "quota exceeded"
300
- self._mark_rpc_backoff(failed_index, self.QUOTA_EXCEEDED_BACKOFF)
365
+ error_type = "QUOTA"
366
+ backoff = self.QUOTA_EXCEEDED_BACKOFF
367
+ self._mark_rpc_backoff(failed_index, backoff)
301
368
  elif result["is_rate_limit"]:
302
- error_type = "rate limit"
303
- self._mark_rpc_backoff(failed_index, self.RATE_LIMIT_BACKOFF)
369
+ error_type = "RATE_LIMIT"
370
+ backoff = self.RATE_LIMIT_BACKOFF
371
+ self._mark_rpc_backoff(failed_index, backoff)
304
372
  # Brief global backoff so other threads don't immediately flood
305
373
  # the same (now backed-off) RPC before rotation takes effect.
306
374
  self._rate_limiter.trigger_backoff(seconds=2.0)
307
375
  else:
308
- error_type = "connection"
309
- self._mark_rpc_backoff(failed_index, self.CONNECTION_ERROR_BACKOFF)
376
+ error_type = "CONNECTION"
377
+ backoff = self.CONNECTION_ERROR_BACKOFF
378
+ self._mark_rpc_backoff(failed_index, backoff)
379
+
380
+ # Count healthy RPCs for visibility
381
+ healthy_count = sum(1 for i in range(len(self.chain.rpcs)) if self._is_rpc_healthy(i))
382
+ total_rpcs = len(self.chain.rpcs) if self.chain.rpcs else 0
310
383
 
311
384
  logger.warning(
312
- f"RPC {error_type} error on {self.chain.name} "
313
- f"(RPC #{failed_index}): {error}"
385
+ f"[{self.chain.name}] RPC #{failed_index} {error_type} → "
386
+ f"backoff {int(backoff)}s ({healthy_count}/{total_rpcs} healthy) | "
387
+ f"{failed_rpc}: {str(error)[:100]}"
314
388
  )
315
389
 
316
390
  if self.rotate_rpc():
317
391
  result["rotated"] = True
318
392
  result["should_retry"] = True
319
- logger.info(f"Rotated to RPC #{self._current_rpc_index} for {self.chain.name}")
393
+ new_rpc = sanitize_rpc_url(self.chain.rpcs[self._current_rpc_index])
394
+ logger.info(f"[{self.chain.name}] Rotated to RPC #{self._current_rpc_index}: {new_rpc}")
320
395
  else:
321
396
  # Rotation skipped (cooldown or single RPC) - still allow retry
322
397
  result["should_retry"] = True
323
- logger.info(
324
- f"RPC rotation skipped, retrying with current RPC #{self._current_rpc_index}"
398
+ logger.debug(
399
+ f"[{self.chain.name}] Rotation skipped (cooldown), retrying RPC #{self._current_rpc_index}"
325
400
  )
326
401
 
327
402
  elif result["is_server_error"]:
@@ -36,3 +36,11 @@ class ChainInterfaces:
36
36
  for name, interface in self.items():
37
37
  results[name] = interface.check_rpc_health()
38
38
  return results
39
+
40
+ def close_all(self) -> None:
41
+ """Close all chain interface sessions.
42
+
43
+ Call this at application shutdown to release network resources.
44
+ """
45
+ for _, interface in self.items():
46
+ interface.close()
@@ -0,0 +1,299 @@
1
+ """Module for fetching and parsing RPCs from Chainlist.org."""
2
+
3
+ import json
4
+ import time
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ import requests
10
+
11
+ from iwa.core.constants import CACHE_DIR
12
+ from iwa.core.utils import configure_logger
13
+
14
+ logger = configure_logger()
15
+
16
+ # -- RPC probing constants --------------------------------------------------
17
+
18
+ MAX_CHAINLIST_CANDIDATES = 15 # Probe at most this many candidates
19
+ PROBE_TIMEOUT = 5.0 # Seconds per probe request
20
+ MAX_BLOCK_LAG = 10 # Blocks behind majority → considered stale
21
+
22
+
23
+ def _normalize_url(url: str) -> str:
24
+ """Normalize an RPC URL for deduplication (lowercase, strip trailing slash)."""
25
+ return url.rstrip("/").lower()
26
+
27
+
28
+ def _is_template_url(url: str) -> bool:
29
+ """Return True if the URL contains template variables requiring an API key."""
30
+ return "${" in url or "{" in url
31
+
32
+
33
+ def probe_rpc(
34
+ url: str,
35
+ timeout: float = PROBE_TIMEOUT,
36
+ session: Optional[requests.Session] = None,
37
+ ) -> Optional[Tuple[str, float, int]]:
38
+ """Probe an RPC endpoint with eth_blockNumber.
39
+
40
+ Returns ``(url, latency_ms, block_number)`` on success, or ``None``
41
+ if the endpoint is unreachable, slow, or returns invalid data.
42
+
43
+ Args:
44
+ url: The RPC endpoint URL to probe.
45
+ timeout: Request timeout in seconds.
46
+ session: Optional requests.Session for connection reuse. If None,
47
+ creates a temporary session that is properly closed.
48
+
49
+ """
50
+ # Use provided session or create temporary one with proper cleanup
51
+ own_session = session is None
52
+ if own_session:
53
+ session = requests.Session()
54
+
55
+ try:
56
+ start = time.monotonic()
57
+ resp = session.post(
58
+ url,
59
+ json={"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1},
60
+ timeout=timeout,
61
+ )
62
+ latency_ms = (time.monotonic() - start) * 1000
63
+ data = resp.json()
64
+ block_hex = data.get("result")
65
+ if not block_hex or not isinstance(block_hex, str) or block_hex == "0x0":
66
+ return None
67
+ return (url, latency_ms, int(block_hex, 16))
68
+ except Exception:
69
+ return None
70
+ finally:
71
+ if own_session:
72
+ session.close()
73
+
74
+
75
+ def _filter_candidates(
76
+ nodes: "List[RPCNode]",
77
+ existing_normalized: set,
78
+ ) -> List[str]:
79
+ """Filter ChainList nodes to usable HTTPS candidates."""
80
+ candidates: List[str] = []
81
+ for node in nodes:
82
+ url = node.url
83
+ if not url.startswith("https://"):
84
+ continue
85
+ if _is_template_url(url):
86
+ continue
87
+ if _normalize_url(url) in existing_normalized:
88
+ continue
89
+ candidates.append(url)
90
+ if len(candidates) >= MAX_CHAINLIST_CANDIDATES:
91
+ break
92
+ return candidates
93
+
94
+
95
+ def _probe_candidates(
96
+ candidates: List[str],
97
+ ) -> List[Tuple[str, float, int]]:
98
+ """Probe a list of RPC URLs in parallel, returning successful results.
99
+
100
+ Uses a shared session for all probes to enable connection pooling and
101
+ ensure proper cleanup of all connections when probing completes.
102
+ """
103
+ results: List[Tuple[str, float, int]] = []
104
+ # Use a shared session with connection pooling for all probes
105
+ # This prevents FD leaks from individual probe connections
106
+ with requests.Session() as session:
107
+ # Configure connection pool size to match our max workers
108
+ adapter = requests.adapters.HTTPAdapter(
109
+ pool_connections=10,
110
+ pool_maxsize=10,
111
+ max_retries=0, # No retries - we handle failure gracefully
112
+ )
113
+ session.mount("https://", adapter)
114
+ session.mount("http://", adapter)
115
+
116
+ with ThreadPoolExecutor(max_workers=min(len(candidates), 10)) as pool:
117
+ futures = {
118
+ pool.submit(probe_rpc, url, PROBE_TIMEOUT, session): url
119
+ for url in candidates
120
+ }
121
+ for future in as_completed(futures, timeout=15):
122
+ try:
123
+ result = future.result()
124
+ if result is not None:
125
+ results.append(result)
126
+ except Exception:
127
+ pass
128
+ # Session is closed here via context manager, releasing all connections
129
+ return results
130
+
131
+
132
+ def _rank_and_select(
133
+ results: List[Tuple[str, float, int]],
134
+ candidates: List[str],
135
+ chain_id: int,
136
+ max_results: int,
137
+ ) -> List[str]:
138
+ """Rank probed RPCs by latency, filtering stale ones."""
139
+ blocks = sorted(r[2] for r in results)
140
+ median_block = blocks[len(blocks) // 2]
141
+
142
+ valid = [
143
+ (url, latency)
144
+ for url, latency, block in results
145
+ if median_block - block <= MAX_BLOCK_LAG
146
+ ]
147
+ valid.sort(key=lambda x: x[1])
148
+
149
+ selected = [url for url, _ in valid[:max_results]]
150
+ if selected:
151
+ logger.info(
152
+ f"ChainList: validated {len(selected)}/{len(candidates)} "
153
+ f"candidates for chain {chain_id} "
154
+ f"(median block: {median_block})"
155
+ )
156
+ return selected
157
+
158
+
159
+ @dataclass
160
+ class RPCNode:
161
+ """Represents a single RPC node with its properties."""
162
+
163
+ url: str
164
+ is_working: bool
165
+ privacy: Optional[str] = None
166
+ tracking: Optional[str] = None
167
+
168
+ @property
169
+ def is_tracking(self) -> bool:
170
+ """Returns True if the RPC is known to track user data."""
171
+ return self.privacy == "privacy" or self.tracking in ("limited", "yes")
172
+
173
+
174
+ class ChainlistRPC:
175
+ """Fetcher and parser for Chainlist RPC data."""
176
+
177
+ URL = "https://chainlist.org/rpcs.json"
178
+ CACHE_PATH = CACHE_DIR / "chainlist_rpcs.json"
179
+ CACHE_TTL = 86400 # 24 hours
180
+
181
+ def __init__(self) -> None:
182
+ """Initialize the ChainlistRPC instance."""
183
+ self._data: List[Dict[str, Any]] = []
184
+
185
+ def fetch_data(self, force_refresh: bool = False) -> None:
186
+ """Fetches the RPC data from Chainlist with local caching."""
187
+ # 1. Try local cache first unless force_refresh is requested
188
+ if not force_refresh and self.CACHE_PATH.exists():
189
+ try:
190
+ mtime = self.CACHE_PATH.stat().st_mtime
191
+ if time.time() - mtime < self.CACHE_TTL:
192
+ with self.CACHE_PATH.open("r") as f:
193
+ self._data = json.load(f)
194
+ if self._data:
195
+ return
196
+ except Exception as e:
197
+ print(f"Error reading Chainlist cache: {e}")
198
+
199
+ # 2. Fetch from remote (use session context for proper cleanup)
200
+ try:
201
+ with requests.Session() as session:
202
+ response = session.get(self.URL, timeout=10)
203
+ response.raise_for_status()
204
+ self._data = response.json()
205
+
206
+ # 3. Update local cache
207
+ if self._data:
208
+ self.CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
209
+ with self.CACHE_PATH.open("w") as f:
210
+ json.dump(self._data, f)
211
+ except requests.RequestException as e:
212
+ print(f"Error fetching Chainlist data from {self.URL}: {e}")
213
+ # Fallback to expired cache if available
214
+ if not self._data and self.CACHE_PATH.exists():
215
+ try:
216
+ with self.CACHE_PATH.open("r") as f:
217
+ self._data = json.load(f)
218
+ except Exception:
219
+ pass
220
+ if not self._data:
221
+ self._data = []
222
+
223
+ def get_chain_data(self, chain_id: int) -> Optional[Dict[str, Any]]:
224
+ """Returns the raw chain data for a specific chain ID."""
225
+ if not self._data:
226
+ self.fetch_data()
227
+
228
+ for entry in self._data:
229
+ if entry.get("chainId") == chain_id:
230
+ return entry
231
+ return None
232
+
233
+ def get_rpcs(self, chain_id: int) -> List[RPCNode]:
234
+ """Returns a list of RPCNode objects for a parsed and cleaner view."""
235
+ chain_data = self.get_chain_data(chain_id)
236
+ if not chain_data:
237
+ return []
238
+
239
+ raw_rpcs = chain_data.get("rpc", [])
240
+ nodes = []
241
+ for rpc in raw_rpcs:
242
+ nodes.append(
243
+ RPCNode(
244
+ url=rpc.get("url", ""),
245
+ is_working=True,
246
+ privacy=rpc.get("privacy"),
247
+ tracking=rpc.get("tracking"),
248
+ )
249
+ )
250
+ return nodes
251
+
252
+ def get_https_rpcs(self, chain_id: int) -> List[str]:
253
+ """Returns a list of HTTPS RPC URLs for the given chain."""
254
+ rpcs = self.get_rpcs(chain_id)
255
+ return [
256
+ node.url
257
+ for node in rpcs
258
+ if node.url.startswith("https://") or node.url.startswith("http://")
259
+ ]
260
+
261
+ def get_wss_rpcs(self, chain_id: int) -> List[str]:
262
+ """Returns a list of WSS RPC URLs for the given chain."""
263
+ rpcs = self.get_rpcs(chain_id)
264
+ return [
265
+ node.url
266
+ for node in rpcs
267
+ if node.url.startswith("wss://") or node.url.startswith("ws://")
268
+ ]
269
+
270
+ def get_validated_rpcs(
271
+ self,
272
+ chain_id: int,
273
+ existing_rpcs: List[str],
274
+ max_results: int = 5,
275
+ ) -> List[str]:
276
+ """Return ChainList RPCs filtered, probed, and sorted by quality.
277
+
278
+ 1. Fetch HTTPS RPCs from ChainList for *chain_id*.
279
+ 2. Filter out template URLs, duplicates of *existing_rpcs*, and
280
+ websocket endpoints.
281
+ 3. Probe the top candidates in parallel with ``eth_blockNumber``.
282
+ 4. Discard RPCs that are stale (block number lagging behind majority).
283
+ 5. Return up to *max_results* URLs sorted by latency (fastest first).
284
+ """
285
+ nodes = self.get_rpcs(chain_id)
286
+ if not nodes:
287
+ return []
288
+
289
+ existing_normalized = {_normalize_url(u) for u in existing_rpcs}
290
+ candidates = _filter_candidates(nodes, existing_normalized)
291
+ if not candidates:
292
+ return []
293
+
294
+ results = _probe_candidates(candidates)
295
+ if not results:
296
+ return []
297
+
298
+ selected = _rank_and_select(results, candidates, chain_id, max_results)
299
+ return selected