iwa 0.0.58__tar.gz → 0.0.59__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 (234) hide show
  1. {iwa-0.0.58/src/iwa.egg-info → iwa-0.0.59}/PKG-INFO +1 -1
  2. {iwa-0.0.58 → iwa-0.0.59}/pyproject.toml +2 -2
  3. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/interface.py +32 -21
  4. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/rate_limiter.py +0 -6
  5. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chainlist.py +15 -10
  6. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/cli.py +3 -0
  7. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/cache.py +1 -1
  8. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/contract.py +1 -0
  9. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/decoder.py +10 -4
  10. iwa-0.0.59/src/iwa/core/http.py +31 -0
  11. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/ipfs.py +11 -19
  12. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/keys.py +10 -4
  13. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/models.py +1 -3
  14. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/pricing.py +3 -21
  15. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/rpc_monitor.py +1 -0
  16. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/balance.py +0 -1
  17. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/safe.py +8 -2
  18. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/safe_executor.py +52 -18
  19. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transaction.py +32 -12
  20. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/erc20.py +0 -1
  21. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/native.py +1 -1
  22. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tests/test_gnosis_fee.py +6 -2
  23. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tests/test_ipfs.py +1 -1
  24. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tests/test_regression_fixes.py +3 -6
  25. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/utils.py +2 -0
  26. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/wallet.py +3 -1
  27. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/constants.py +15 -5
  28. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/activity_checker.py +3 -3
  29. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/staking.py +0 -1
  30. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/events.py +15 -13
  31. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/importer.py +26 -20
  32. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/plugin.py +16 -14
  33. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/drain.py +1 -3
  34. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/lifecycle.py +9 -9
  35. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/staking.py +11 -6
  36. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_archiving.py +25 -15
  37. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_integration.py +49 -29
  38. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager.py +8 -10
  39. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +5 -4
  40. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +6 -5
  41. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_staking.py +64 -38
  42. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/drain_accounts.py +2 -1
  43. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/reset_env.py +2 -1
  44. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/test_chainlist.py +5 -1
  45. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/screens/wallets.py +1 -3
  46. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/services.py +10 -5
  47. {iwa-0.0.58 → iwa-0.0.59/src/iwa.egg-info}/PKG-INFO +1 -1
  48. {iwa-0.0.58 → iwa-0.0.59}/src/iwa.egg-info/SOURCES.txt +1 -0
  49. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_balance_service.py +0 -2
  50. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_chain.py +1 -2
  51. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rate_limiter_retry.py +2 -7
  52. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rpc_efficiency.py +4 -1
  53. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rpc_rate_limit.py +1 -0
  54. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rpc_rotation.py +4 -4
  55. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_safe_executor.py +76 -50
  56. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_safe_integration.py +11 -6
  57. {iwa-0.0.58 → iwa-0.0.59}/LICENSE +0 -0
  58. {iwa-0.0.58 → iwa-0.0.59}/README.md +0 -0
  59. {iwa-0.0.58 → iwa-0.0.59}/setup.cfg +0 -0
  60. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/__init__.py +0 -0
  61. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/__main__.py +0 -0
  62. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/__init__.py +0 -0
  63. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/__init__.py +0 -0
  64. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/errors.py +0 -0
  65. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/manager.py +0 -0
  66. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/chain/models.py +0 -0
  67. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/constants.py +0 -0
  68. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/__init__.py +0 -0
  69. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/abis/erc20.json +0 -0
  70. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/abis/multisend.json +0 -0
  71. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
  72. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/erc20.py +0 -0
  73. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/contracts/multisend.py +0 -0
  74. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/db.py +0 -0
  75. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/mnemonic.py +0 -0
  76. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/monitor.py +0 -0
  77. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/plugins.py +0 -0
  78. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/secrets.py +0 -0
  79. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/__init__.py +0 -0
  80. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/account.py +0 -0
  81. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/plugin.py +0 -0
  82. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/__init__.py +0 -0
  83. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/base.py +0 -0
  84. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/multisend.py +0 -0
  85. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/services/transfer/swap.py +0 -0
  86. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tables.py +0 -0
  87. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/test.py +0 -0
  88. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tests/test_pricing.py +0 -0
  89. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/tests/test_wallet.py +0 -0
  90. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/types.py +0 -0
  91. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/core/ui.py +0 -0
  92. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/__init__.py +0 -0
  93. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/__init__.py +0 -0
  94. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
  95. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
  96. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
  97. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/types.py +0 -0
  98. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
  99. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/plugin.py +0 -0
  100. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/safe.py +0 -0
  101. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
  102. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
  103. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/__init__.py +0 -0
  104. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
  105. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
  106. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
  107. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
  108. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
  109. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
  110. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
  111. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
  112. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
  113. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
  114. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/base.py +0 -0
  115. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech.py +0 -0
  116. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
  117. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
  118. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/service.py +0 -0
  119. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/mech_reference.py +0 -0
  120. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/models.py +0 -0
  121. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
  122. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
  123. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
  124. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/base.py +0 -0
  125. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
  126. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/conftest.py +0 -0
  127. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
  128. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
  129. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
  130. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
  131. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
  132. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
  133. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
  134. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
  135. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
  136. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
  137. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
  138. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
  139. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
  140. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
  141. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
  142. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
  143. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tui/__init__.py +0 -0
  144. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
  145. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/__init__.py +0 -0
  146. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/check_profile.py +0 -0
  147. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/list_contracts.py +0 -0
  148. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/release.py +0 -0
  149. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/reset_tenderly.py +0 -0
  150. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/restore_backup.py +0 -0
  151. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tools/wallet_check.py +0 -0
  152. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/__init__.py +0 -0
  153. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/app.py +0 -0
  154. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/modals/__init__.py +0 -0
  155. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/modals/base.py +0 -0
  156. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/rpc.py +0 -0
  157. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/screens/__init__.py +0 -0
  158. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/tests/test_app.py +0 -0
  159. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/tests/test_rpc.py +0 -0
  160. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
  161. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/tests/test_widgets.py +0 -0
  162. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/widgets/__init__.py +0 -0
  163. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/widgets/base.py +0 -0
  164. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/tui/workers.py +0 -0
  165. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/dependencies.py +0 -0
  166. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/models.py +0 -0
  167. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/accounts.py +0 -0
  168. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/__init__.py +0 -0
  169. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/admin.py +0 -0
  170. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/funding.py +0 -0
  171. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/general.py +0 -0
  172. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/olas/staking.py +0 -0
  173. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/state.py +0 -0
  174. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/swap.py +0 -0
  175. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/routers/transactions.py +0 -0
  176. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/server.py +0 -0
  177. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/static/app.js +0 -0
  178. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/static/index.html +0 -0
  179. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/static/style.css +0 -0
  180. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/tests/test_web_endpoints.py +0 -0
  181. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/tests/test_web_olas.py +0 -0
  182. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/tests/test_web_swap.py +0 -0
  183. {iwa-0.0.58 → iwa-0.0.59}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
  184. {iwa-0.0.58 → iwa-0.0.59}/src/iwa.egg-info/dependency_links.txt +0 -0
  185. {iwa-0.0.58 → iwa-0.0.59}/src/iwa.egg-info/entry_points.txt +0 -0
  186. {iwa-0.0.58 → iwa-0.0.59}/src/iwa.egg-info/requires.txt +0 -0
  187. {iwa-0.0.58 → iwa-0.0.59}/src/iwa.egg-info/top_level.txt +0 -0
  188. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_cow.py +0 -0
  189. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_safe.py +0 -0
  190. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_transaction_retry_logic.py +0 -0
  191. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_tui.py +0 -0
  192. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_wallets_screen.py +0 -0
  193. {iwa-0.0.58 → iwa-0.0.59}/src/tests/legacy_web.py +0 -0
  194. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_account_service.py +0 -0
  195. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_chain_interface.py +0 -0
  196. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_chain_interface_coverage.py +0 -0
  197. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_cli.py +0 -0
  198. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_contract.py +0 -0
  199. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_db.py +0 -0
  200. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_drain_coverage.py +0 -0
  201. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_erc20.py +0 -0
  202. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_gnosis_plugin.py +0 -0
  203. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_keys.py +0 -0
  204. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_legacy_wallet.py +0 -0
  205. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_main.py +0 -0
  206. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_migration.py +0 -0
  207. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_mnemonic.py +0 -0
  208. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_modals.py +0 -0
  209. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_models.py +0 -0
  210. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_monitor.py +0 -0
  211. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_multisend.py +0 -0
  212. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_plugin_service.py +0 -0
  213. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rate_limiter.py +0 -0
  214. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_reset_tenderly.py +0 -0
  215. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_rpc_view.py +0 -0
  216. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_safe_coverage.py +0 -0
  217. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_safe_service.py +0 -0
  218. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_service_manager_integration.py +0 -0
  219. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_service_manager_structure.py +0 -0
  220. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_service_transaction.py +0 -0
  221. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_staking_router.py +0 -0
  222. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_staking_simple.py +0 -0
  223. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_tables.py +0 -0
  224. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transaction_service.py +0 -0
  225. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transfer_multisend.py +0 -0
  226. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transfer_native.py +0 -0
  227. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transfer_security.py +0 -0
  228. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transfer_structure.py +0 -0
  229. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_transfer_swap_unit.py +0 -0
  230. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_ui_coverage.py +0 -0
  231. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_utils.py +0 -0
  232. {iwa-0.0.58 → iwa-0.0.59}/src/tests/test_workers.py +0 -0
  233. {iwa-0.0.58 → iwa-0.0.59}/src/tools/create_and_stake_service.py +0 -0
  234. {iwa-0.0.58 → iwa-0.0.59}/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.58
3
+ Version: 0.0.59
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.58"
3
+ version = "0.0.59"
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.58"
75
+ target-version = "0.0.59"
76
76
  fix = true
77
77
 
78
78
  [tool.ruff.lint]
@@ -4,6 +4,7 @@ import threading
4
4
  import time
5
5
  from typing import Callable, Dict, Optional, TypeVar, Union
6
6
 
7
+ import requests
7
8
  from web3 import Web3
8
9
 
9
10
  from iwa.core.chain.errors import TenderlyQuotaExceededError, sanitize_rpc_url
@@ -23,6 +24,7 @@ class ChainInterface:
23
24
 
24
25
  DEFAULT_MAX_RETRIES = 6 # Allow trying most/all available RPCs on rate limit
25
26
  DEFAULT_RETRY_DELAY = 1.0 # Base delay between retries (exponential backoff)
27
+ ROTATION_COOLDOWN_SECONDS = 2.0 # Minimum time between RPC rotations
26
28
 
27
29
  chain: SupportedChain
28
30
 
@@ -48,6 +50,7 @@ class ChainInterface:
48
50
 
49
51
  self._initial_block = 0
50
52
  self._rotation_lock = threading.Lock()
53
+ self._session = requests.Session()
51
54
  self._init_web3()
52
55
 
53
56
  @property
@@ -289,20 +292,17 @@ class ChainInterface:
289
292
 
290
293
  def rotate_rpc(self) -> bool:
291
294
  """Rotate to the next available RPC."""
292
- # Minimum time between rotations to prevent cascade rotations from parallel requests
293
- # failing simultaneously
294
- cooldown_seconds = 2.0
295
-
296
295
  with self._rotation_lock:
297
296
  if not self.chain.rpcs or len(self.chain.rpcs) <= 1:
298
297
  return False
299
298
 
300
299
  # Cooldown: prevent cascade rotations from in-flight requests
301
300
  now = time.monotonic()
302
- if now - self._last_rotation_time < cooldown_seconds:
301
+ elapsed = now - self._last_rotation_time
302
+ if elapsed < self.ROTATION_COOLDOWN_SECONDS:
303
303
  logger.debug(
304
304
  f"RPC rotation skipped for {self.chain.name} (cooldown active, "
305
- f"{cooldown_seconds - (now - self._last_rotation_time):.1f}s remaining)"
305
+ f"{self.ROTATION_COOLDOWN_SECONDS - elapsed:.1f}s remaining)"
306
306
  )
307
307
  return False
308
308
 
@@ -326,7 +326,11 @@ class ChainInterface:
326
326
  def _init_web3_under_lock(self):
327
327
  """Internal non-thread-safe web3 initialization."""
328
328
  rpc_url = self.chain.rpcs[self._current_rpc_index] if self.chain.rpcs else ""
329
- raw_web3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": DEFAULT_RPC_TIMEOUT}))
329
+ raw_web3 = Web3(
330
+ Web3.HTTPProvider(
331
+ rpc_url, request_kwargs={"timeout": DEFAULT_RPC_TIMEOUT}, session=self._session
332
+ )
333
+ )
330
334
 
331
335
  # Use duck typing to check if current web3 is a RateLimitedWeb3 wrapper
332
336
  if hasattr(self, "web3") and hasattr(self.web3, "set_backend"):
@@ -409,7 +413,9 @@ class ChainInterface:
409
413
  except Exception:
410
414
  return address[:6] + "..." + address[-4:]
411
415
 
412
- def get_token_decimals(self, address: EthereumAddress, fallback_to_18: bool = True) -> Optional[int]:
416
+ def get_token_decimals(
417
+ self, address: EthereumAddress, fallback_to_18: bool = True
418
+ ) -> Optional[int]:
413
419
  """Get token decimals for an address.
414
420
 
415
421
  Args:
@@ -426,7 +432,15 @@ class ChainInterface:
426
432
  # Use _web3 directly to ensure current provider after RPC rotation
427
433
  contract = self.web3._web3.eth.contract(
428
434
  address=self.web3.to_checksum_address(address),
429
- abi=[{"constant": True, "inputs": [], "name": "decimals", "outputs": [{"type": "uint8"}], "type": "function"}]
435
+ abi=[
436
+ {
437
+ "constant": True,
438
+ "inputs": [],
439
+ "name": "decimals",
440
+ "outputs": [{"type": "uint8"}],
441
+ "type": "function",
442
+ }
443
+ ],
430
444
  )
431
445
  return contract.functions.decimals().call()
432
446
  except Exception:
@@ -475,8 +489,10 @@ class ChainInterface:
475
489
  # Contract calls already have the target address in the contract object
476
490
  if not built_method and "to" in tx_params:
477
491
  params["to"] = tx_params["to"]
478
- elif not built_method and "to" in params: # Fallback if added to params earlier (though not here yet)
479
- pass
492
+ elif (
493
+ not built_method and "to" in params
494
+ ): # Fallback if added to params earlier (though not here yet)
495
+ pass
480
496
 
481
497
  # Determine gas
482
498
  if built_method:
@@ -489,11 +505,7 @@ class ChainInterface:
489
505
  # Native transfer - dynamic estimation
490
506
  try:
491
507
  # web3.eth.estimate_gas returns gas for the dict it receives
492
- est_params = {
493
- "from": params["from"],
494
- "to": params["to"],
495
- "value": params["value"]
496
- }
508
+ est_params = {"from": params["from"], "to": params["to"], "value": params["value"]}
497
509
  # Remove None 'to' for contract creation simulation if needed, but usually send() has to
498
510
  if not est_params["to"]:
499
511
  est_params.pop("to")
@@ -501,7 +513,9 @@ class ChainInterface:
501
513
  estimated = self.web3.eth.estimate_gas(est_params)
502
514
  # Apply 10% buffer for safety
503
515
  params["gas"] = int(estimated * 1.1)
504
- logger.debug(f"[GAS] Estimated native transfer gas: {params['gas']} (raw: {estimated})")
516
+ logger.debug(
517
+ f"[GAS] Estimated native transfer gas: {params['gas']} (raw: {estimated})"
518
+ )
505
519
  except Exception as e:
506
520
  logger.debug(f"[GAS] Native estimation failed, fallback to 21000: {e}")
507
521
  params["gas"] = 21_000
@@ -533,10 +547,7 @@ class ChainInterface:
533
547
  # Buffer max_fee to handle base fee expansion
534
548
  max_fee = int(base_fee * 1.5) + max_priority_fee
535
549
 
536
- return {
537
- "maxFeePerGas": max_fee,
538
- "maxPriorityFeePerGas": max_priority_fee
539
- }
550
+ return {"maxFeePerGas": max_fee, "maxPriorityFeePerGas": max_priority_fee}
540
551
  except Exception as e:
541
552
  logger.debug(f"Failed to calculate EIP-1559 fees: {e}, falling back to legacy")
542
553
 
@@ -188,12 +188,10 @@ class RateLimitedEth:
188
188
 
189
189
  def _execute_with_retry(self, method, method_name, *args, **kwargs):
190
190
  """Execute read operation with retry logic."""
191
- last_error = None
192
191
  for attempt in range(self.DEFAULT_READ_RETRIES + 1):
193
192
  try:
194
193
  return method(*args, **kwargs)
195
194
  except Exception as e:
196
- last_error = e
197
195
  # Use chain interface to handle error (logging, rotation, etc.)
198
196
  result = self._chain_interface._handle_rpc_error(e)
199
197
 
@@ -206,10 +204,6 @@ class RateLimitedEth:
206
204
  )
207
205
  time.sleep(delay)
208
206
 
209
- if last_error:
210
- raise last_error
211
- raise RuntimeError(f"{method_name} failed unexpectedly")
212
-
213
207
 
214
208
  class RateLimitedWeb3:
215
209
  """Wrapper around Web3 instance that applies rate limiting transparently."""
@@ -1,4 +1,5 @@
1
1
  """Module for fetching and parsing RPCs from Chainlist.org."""
2
+
2
3
  import json
3
4
  import time
4
5
  from dataclasses import dataclass
@@ -78,7 +79,7 @@ class ChainlistRPC:
78
79
  self.fetch_data()
79
80
 
80
81
  for entry in self._data:
81
- if entry.get('chainId') == chain_id:
82
+ if entry.get("chainId") == chain_id:
82
83
  return entry
83
84
  return None
84
85
 
@@ -88,22 +89,25 @@ class ChainlistRPC:
88
89
  if not chain_data:
89
90
  return []
90
91
 
91
- raw_rpcs = chain_data.get('rpc', [])
92
+ raw_rpcs = chain_data.get("rpc", [])
92
93
  nodes = []
93
94
  for rpc in raw_rpcs:
94
- nodes.append(RPCNode(
95
- url=rpc.get('url', ''),
96
- is_working=True,
97
- privacy=rpc.get('privacy'),
98
- tracking=rpc.get('tracking')
99
- ))
95
+ nodes.append(
96
+ RPCNode(
97
+ url=rpc.get("url", ""),
98
+ is_working=True,
99
+ privacy=rpc.get("privacy"),
100
+ tracking=rpc.get("tracking"),
101
+ )
102
+ )
100
103
  return nodes
101
104
 
102
105
  def get_https_rpcs(self, chain_id: int) -> List[str]:
103
106
  """Returns a list of HTTPS RPC URLs for the given chain."""
104
107
  rpcs = self.get_rpcs(chain_id)
105
108
  return [
106
- node.url for node in rpcs
109
+ node.url
110
+ for node in rpcs
107
111
  if node.url.startswith("https://") or node.url.startswith("http://")
108
112
  ]
109
113
 
@@ -111,6 +115,7 @@ class ChainlistRPC:
111
115
  """Returns a list of WSS RPC URLs for the given chain."""
112
116
  rpcs = self.get_rpcs(chain_id)
113
117
  return [
114
- node.url for node in rpcs
118
+ node.url
119
+ for node in rpcs
115
120
  if node.url.startswith("wss://") or node.url.startswith("ws://")
116
121
  ]
@@ -16,13 +16,16 @@ from iwa.tui.app import IwaApp
16
16
 
17
17
  iwa_cli = typer.Typer(help="iwa command line interface")
18
18
 
19
+
19
20
  @iwa_cli.callback()
20
21
  def main_callback(ctx: typer.Context):
21
22
  """Initialize IWA CLI."""
22
23
  # Print banner on startup
23
24
  from iwa.core.utils import get_version, print_banner
25
+
24
26
  print_banner("iwa", get_version("iwa"))
25
27
 
28
+
26
29
  wallet_cli = typer.Typer(help="Manage wallet")
27
30
 
28
31
  iwa_cli.add_typer(wallet_cli, name="wallet")
@@ -62,7 +62,7 @@ class ContractCache:
62
62
 
63
63
  key = self._make_key(contract_cls, address, chain_name)
64
64
  now = time.time()
65
- expiry = (ttl if ttl is not None else self.ttl)
65
+ expiry = ttl if ttl is not None else self.ttl
66
66
 
67
67
  with self._lock:
68
68
  # Check if cached and valid
@@ -31,6 +31,7 @@ def clear_abi_cache() -> None:
31
31
  global _ABI_CACHE
32
32
  _ABI_CACHE = {}
33
33
 
34
+
34
35
  # Panic codes (from Solidity)
35
36
  # ... (rest of PANIC_CODES) ...
36
37
  PANIC_CODES = {
@@ -58,7 +58,7 @@ class ErrorDecoder:
58
58
  # Also check core ABIs if they are in a different place
59
59
  core_abi_path = src_root / "iwa" / "core" / "contracts" / "abis"
60
60
  if core_abi_path.exists() and core_abi_path not in [f.parent for f in abi_files]:
61
- abi_files.extend(list(core_abi_path.glob("*.json")))
61
+ abi_files.extend(list(core_abi_path.glob("*.json")))
62
62
 
63
63
  logger.debug(f"Found {len(abi_files)} ABI files for error decoding.")
64
64
 
@@ -66,7 +66,11 @@ class ErrorDecoder:
66
66
  try:
67
67
  with open(abi_path, "r", encoding="utf-8") as f:
68
68
  content = json.load(f)
69
- abi = content.get("abi") if isinstance(content, dict) and "abi" in content else content
69
+ abi = (
70
+ content.get("abi")
71
+ if isinstance(content, dict) and "abi" in content
72
+ else content
73
+ )
70
74
  if isinstance(abi, list):
71
75
  self._process_abi(abi, abi_path.name)
72
76
  except Exception as e:
@@ -91,7 +95,7 @@ class ErrorDecoder:
91
95
  "types": types,
92
96
  "arg_names": names,
93
97
  "source": source_name,
94
- "signature": signature
98
+ "signature": signature,
95
99
  }
96
100
 
97
101
  if selector not in self._selectors:
@@ -145,7 +149,9 @@ class ErrorDecoder:
145
149
  for d in self._selectors[selector]:
146
150
  try:
147
151
  decoded = decode(d["types"], bytes.fromhex(encoded_args))
148
- args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
152
+ args_str = ", ".join(
153
+ f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False)
154
+ )
149
155
  results.append((d["name"], f"{d['name']}({args_str})", d["source"]))
150
156
  except Exception:
151
157
  # Try next possible decoding for this selector
@@ -0,0 +1,31 @@
1
+ """Shared HTTP session utilities."""
2
+
3
+ import requests
4
+ from requests.adapters import HTTPAdapter
5
+ from urllib3.util.retry import Retry
6
+
7
+ DEFAULT_RETRY_TOTAL = 3
8
+ DEFAULT_BACKOFF_FACTOR = 1
9
+ DEFAULT_STATUS_FORCELIST = [429, 500, 502, 503, 504]
10
+
11
+
12
+ def create_retry_session(
13
+ retries: int = DEFAULT_RETRY_TOTAL,
14
+ backoff_factor: int = DEFAULT_BACKOFF_FACTOR,
15
+ status_forcelist: list[int] | None = None,
16
+ ) -> requests.Session:
17
+ """Create a requests.Session with retry strategy.
18
+
19
+ Used by PriceService, IPFS, and other modules that need
20
+ persistent HTTP connections with automatic retry.
21
+ """
22
+ session = requests.Session()
23
+ retry_strategy = Retry(
24
+ total=retries,
25
+ backoff_factor=backoff_factor,
26
+ status_forcelist=status_forcelist or DEFAULT_STATUS_FORCELIST,
27
+ )
28
+ adapter = HTTPAdapter(max_retries=retry_strategy)
29
+ session.mount("https://", adapter)
30
+ session.mount("http://", adapter)
31
+ return session
@@ -12,14 +12,15 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
12
12
  import aiohttp
13
13
  from multiformats import CID
14
14
 
15
+ from iwa.core.http import create_retry_session
15
16
  from iwa.core.models import Config
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  import requests
19
20
 
20
- # Global session for sync requests
21
+ # Global persistent sessions (reused across calls to prevent FD leaks)
21
22
  _SYNC_SESSION: Optional["requests.Session"] = None
22
-
23
+ _ASYNC_SESSION: Optional[aiohttp.ClientSession] = None
23
24
 
24
25
 
25
26
  def _compute_cid_v1_hex(data: bytes) -> str:
@@ -63,10 +64,13 @@ async def push_to_ipfs_async(
63
64
  form = aiohttp.FormData()
64
65
  form.add_field("file", data, filename="data", content_type="application/octet-stream")
65
66
 
66
- async with aiohttp.ClientSession() as session:
67
- async with session.post(endpoint, data=form, params=params) as response:
68
- response.raise_for_status()
69
- result = await response.json()
67
+ global _ASYNC_SESSION
68
+ if _ASYNC_SESSION is None or _ASYNC_SESSION.closed:
69
+ _ASYNC_SESSION = aiohttp.ClientSession()
70
+
71
+ async with _ASYNC_SESSION.post(endpoint, data=form, params=params) as response:
72
+ response.raise_for_status()
73
+ result = await response.json()
70
74
 
71
75
  cid_str = result["Hash"]
72
76
  cid = CID.decode(cid_str)
@@ -90,22 +94,10 @@ def push_to_ipfs_sync(
90
94
  :param pin: Whether to pin the content (default True).
91
95
  :return: Tuple of (CIDv1 string, CIDv1 hex representation).
92
96
  """
93
- import requests
94
- from requests.adapters import HTTPAdapter
95
- from urllib3.util.retry import Retry
96
-
97
97
  global _SYNC_SESSION
98
98
 
99
99
  if _SYNC_SESSION is None:
100
- _SYNC_SESSION = requests.Session()
101
- retry_strategy = Retry(
102
- total=3,
103
- backoff_factor=1,
104
- status_forcelist=[429, 500, 502, 503, 504],
105
- )
106
- adapter = HTTPAdapter(max_retries=retry_strategy)
107
- _SYNC_SESSION.mount("http://", adapter)
108
- _SYNC_SESSION.mount("https://", adapter)
100
+ _SYNC_SESSION = create_retry_session()
109
101
 
110
102
  url = api_url or Config().core.ipfs_api_url
111
103
  endpoint = f"{url}/api/v0/add"
@@ -249,7 +249,7 @@ class KeyStorage(BaseModel):
249
249
 
250
250
  with open(self._path, "w", encoding="utf-8") as f:
251
251
  # Use mode='json' to ensure all types (EthereumAddress) are correctly serialized
252
- json.dump(self.model_dump(mode='json'), f, indent=4)
252
+ json.dump(self.model_dump(mode="json"), f, indent=4)
253
253
  f.flush()
254
254
  os.fsync(f.fileno()) # Force write to disk (critical for Docker volumes)
255
255
 
@@ -376,10 +376,14 @@ class KeyStorage(BaseModel):
376
376
  # Check for duplicate tags
377
377
  for existing in self.accounts.values():
378
378
  if existing.tag == account.tag and existing.address != account.address:
379
- raise ValueError(f"Tag '{account.tag}' is already used by address {existing.address}")
379
+ raise ValueError(
380
+ f"Tag '{account.tag}' is already used by address {existing.address}"
381
+ )
380
382
 
381
383
  self.accounts[account.address] = account
382
- logger.info(f"[KeyStorage] Registering account: tag='{account.tag}', address={account.address}")
384
+ logger.info(
385
+ f"[KeyStorage] Registering account: tag='{account.tag}', address={account.address}"
386
+ )
383
387
  self.save()
384
388
 
385
389
  def get_pending_mnemonic(self) -> Optional[str]:
@@ -460,7 +464,9 @@ class KeyStorage(BaseModel):
460
464
 
461
465
  old_tag = account.tag
462
466
  account.tag = new_tag
463
- logger.info(f"[KeyStorage] Renaming account: '{old_tag}' -> '{new_tag}' (address={account.address})")
467
+ logger.info(
468
+ f"[KeyStorage] Renaming account: '{old_tag}' -> '{new_tag}' (address={account.address})"
469
+ )
464
470
  self.save()
465
471
 
466
472
  def _get_private_key(self, address: str) -> Optional[str]:
@@ -80,9 +80,7 @@ class CoreConfig(BaseModel):
80
80
  tenderly_olas_funds: float = Field(default=100000.0, description="OLAS amount for vNet funding")
81
81
 
82
82
  # Safe Transaction Retry System
83
- safe_tx_max_retries: int = Field(
84
- default=6, description="Maximum retries for Safe transactions"
85
- )
83
+ safe_tx_max_retries: int = Field(default=6, description="Maximum retries for Safe transactions")
86
84
  safe_tx_gas_buffer: float = Field(
87
85
  default=1.5, description="Gas buffer multiplier for Safe transactions"
88
86
  )
@@ -4,9 +4,9 @@ import time
4
4
  from datetime import datetime, timedelta
5
5
  from typing import Dict, Optional
6
6
 
7
- import requests
8
7
  from loguru import logger
9
8
 
9
+ from iwa.core.http import create_retry_session
10
10
  from iwa.core.secrets import secrets
11
11
 
12
12
  # Global cache shared across all PriceService instances
@@ -28,28 +28,12 @@ class PriceService:
28
28
  if self.secrets.coingecko_api_key
29
29
  else None
30
30
  )
31
- self.session = requests.Session()
32
- # Configure retry strategy
33
- from requests.adapters import HTTPAdapter
34
- from urllib3.util.retry import Retry
35
-
36
- retry_strategy = Retry(
37
- total=3,
38
- backoff_factor=1,
39
- status_forcelist=[429, 500, 502, 503, 504],
40
- )
41
- adapter = HTTPAdapter(max_retries=retry_strategy)
42
- self.session.mount("https://", adapter)
43
- self.session.mount("http://", adapter)
31
+ self.session = create_retry_session()
44
32
 
45
33
  def close(self):
46
34
  """Close the session."""
47
35
  self.session.close()
48
36
 
49
- def __del__(self):
50
- """Cleanup on deletion."""
51
- self.close()
52
-
53
37
  def get_token_price(self, token_id: str, vs_currency: str = "eur") -> Optional[float]:
54
38
  """Get token price in specified currency.
55
39
 
@@ -115,9 +99,7 @@ class PriceService:
115
99
  return float(data[token_id][vs_currency])
116
100
 
117
101
  # If we got response but price not found, it's likely a wrong ID
118
- logger.debug(
119
- f"Price for {token_id} in {vs_currency} not found in response: {data}"
120
- )
102
+ logger.debug(f"Price for {token_id} in {vs_currency} not found in response: {data}")
121
103
  return None
122
104
 
123
105
  except Exception as e:
@@ -1,4 +1,5 @@
1
1
  """RPC Monitor for tracking API usage."""
2
+
2
3
  import threading
3
4
  from collections import defaultdict
4
5
  from typing import Dict
@@ -88,4 +88,3 @@ class BalanceService:
88
88
 
89
89
  contract = ERC20Contract(chain_name=chain_name, address=token_address)
90
90
  return contract.balance_of_wei(account.address)
91
-
@@ -1,6 +1,6 @@
1
1
  """Safe service module."""
2
2
 
3
- from typing import TYPE_CHECKING, List, Optional, Tuple
3
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
4
4
 
5
5
  from loguru import logger
6
6
  from safe_eth.eth import EthereumClient
@@ -35,6 +35,7 @@ class SafeService:
35
35
  """Initialize SafeService."""
36
36
  self.key_storage = key_storage
37
37
  self.account_service = account_service
38
+ self._client_cache: Dict[str, EthereumClient] = {}
38
39
 
39
40
  def create_safe(
40
41
  self,
@@ -102,7 +103,12 @@ class SafeService:
102
103
 
103
104
  # Use ChainInterface which has proper RPC rotation and parsing
104
105
  chain_interface = ChainInterfaces().get(chain_name)
105
- return EthereumClient(chain_interface.current_rpc)
106
+ rpc_url = chain_interface.current_rpc
107
+
108
+ if rpc_url not in self._client_cache:
109
+ self._client_cache[rpc_url] = EthereumClient(rpc_url)
110
+
111
+ return self._client_cache[rpc_url]
106
112
 
107
113
  def _deploy_safe_contract(
108
114
  self,