iwa 0.0.21__tar.gz → 0.0.24__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 (222) hide show
  1. {iwa-0.0.21/src/iwa.egg-info → iwa-0.0.24}/PKG-INFO +1 -1
  2. {iwa-0.0.21 → iwa-0.0.24}/pyproject.toml +2 -2
  3. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/importer.py +261 -37
  4. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/plugin.py +127 -41
  5. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +1 -1
  6. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_plugin_full.py +4 -4
  7. {iwa-0.0.21 → iwa-0.0.24/src/iwa.egg-info}/PKG-INFO +1 -1
  8. {iwa-0.0.21 → iwa-0.0.24}/LICENSE +0 -0
  9. {iwa-0.0.21 → iwa-0.0.24}/README.md +0 -0
  10. {iwa-0.0.21 → iwa-0.0.24}/setup.cfg +0 -0
  11. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/__init__.py +0 -0
  12. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/__main__.py +0 -0
  13. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/__init__.py +0 -0
  14. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/__init__.py +0 -0
  15. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/errors.py +0 -0
  16. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/interface.py +0 -0
  17. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/manager.py +0 -0
  18. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/models.py +0 -0
  19. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chain/rate_limiter.py +0 -0
  20. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/chainlist.py +0 -0
  21. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/cli.py +0 -0
  22. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/constants.py +0 -0
  23. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/__init__.py +0 -0
  24. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/abis/erc20.json +0 -0
  25. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/abis/multisend.json +0 -0
  26. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
  27. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/cache.py +0 -0
  28. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/contract.py +0 -0
  29. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/erc20.py +0 -0
  30. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/contracts/multisend.py +0 -0
  31. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/db.py +0 -0
  32. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/ipfs.py +0 -0
  33. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/keys.py +0 -0
  34. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/mnemonic.py +0 -0
  35. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/models.py +0 -0
  36. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/monitor.py +0 -0
  37. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/plugins.py +0 -0
  38. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/pricing.py +0 -0
  39. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/rpc_monitor.py +0 -0
  40. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/secrets.py +0 -0
  41. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/__init__.py +0 -0
  42. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/account.py +0 -0
  43. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/balance.py +0 -0
  44. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/plugin.py +0 -0
  45. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/safe.py +0 -0
  46. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transaction.py +0 -0
  47. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/__init__.py +0 -0
  48. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/base.py +0 -0
  49. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/erc20.py +0 -0
  50. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/multisend.py +0 -0
  51. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/native.py +0 -0
  52. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/services/transfer/swap.py +0 -0
  53. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/tables.py +0 -0
  54. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/test.py +0 -0
  55. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/tests/test_wallet.py +0 -0
  56. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/types.py +0 -0
  57. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/ui.py +0 -0
  58. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/utils.py +0 -0
  59. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/core/wallet.py +0 -0
  60. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/__init__.py +0 -0
  61. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/__init__.py +0 -0
  62. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
  63. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
  64. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
  65. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/cow/types.py +0 -0
  66. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
  67. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/plugin.py +0 -0
  68. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/safe.py +0 -0
  69. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
  70. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
  71. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/__init__.py +0 -0
  72. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/constants.py +0 -0
  73. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
  74. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
  75. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
  76. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
  77. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
  78. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
  79. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
  80. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
  81. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
  82. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
  83. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
  84. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/base.py +0 -0
  85. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/mech.py +0 -0
  86. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
  87. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
  88. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/service.py +0 -0
  89. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/contracts/staking.py +0 -0
  90. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/events.py +0 -0
  91. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/mech_reference.py +0 -0
  92. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/models.py +0 -0
  93. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
  94. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
  95. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
  96. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/base.py +0 -0
  97. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
  98. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
  99. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
  100. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
  101. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/conftest.py +0 -0
  102. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
  103. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
  104. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
  105. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
  106. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
  107. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
  108. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
  109. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
  110. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
  111. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
  112. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
  113. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
  114. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
  115. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
  116. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
  117. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
  118. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
  119. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
  120. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
  121. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tui/__init__.py +0 -0
  122. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
  123. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/__init__.py +0 -0
  124. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/check_profile.py +0 -0
  125. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/list_contracts.py +0 -0
  126. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/release.py +0 -0
  127. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/reset_env.py +0 -0
  128. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/reset_tenderly.py +0 -0
  129. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/restore_backup.py +0 -0
  130. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/test_chainlist.py +0 -0
  131. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tools/wallet_check.py +0 -0
  132. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/__init__.py +0 -0
  133. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/app.py +0 -0
  134. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/modals/__init__.py +0 -0
  135. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/modals/base.py +0 -0
  136. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/rpc.py +0 -0
  137. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/screens/__init__.py +0 -0
  138. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/screens/wallets.py +0 -0
  139. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/tests/test_app.py +0 -0
  140. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/tests/test_rpc.py +0 -0
  141. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
  142. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/tests/test_widgets.py +0 -0
  143. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/widgets/__init__.py +0 -0
  144. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/widgets/base.py +0 -0
  145. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/tui/workers.py +0 -0
  146. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/dependencies.py +0 -0
  147. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/models.py +0 -0
  148. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/accounts.py +0 -0
  149. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/__init__.py +0 -0
  150. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/admin.py +0 -0
  151. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/funding.py +0 -0
  152. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/general.py +0 -0
  153. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/services.py +0 -0
  154. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/olas/staking.py +0 -0
  155. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/state.py +0 -0
  156. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/swap.py +0 -0
  157. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/routers/transactions.py +0 -0
  158. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/server.py +0 -0
  159. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/static/app.js +0 -0
  160. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/static/index.html +0 -0
  161. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/static/style.css +0 -0
  162. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/tests/test_web_endpoints.py +0 -0
  163. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/tests/test_web_olas.py +0 -0
  164. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/tests/test_web_swap.py +0 -0
  165. {iwa-0.0.21 → iwa-0.0.24}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
  166. {iwa-0.0.21 → iwa-0.0.24}/src/iwa.egg-info/SOURCES.txt +0 -0
  167. {iwa-0.0.21 → iwa-0.0.24}/src/iwa.egg-info/dependency_links.txt +0 -0
  168. {iwa-0.0.21 → iwa-0.0.24}/src/iwa.egg-info/entry_points.txt +0 -0
  169. {iwa-0.0.21 → iwa-0.0.24}/src/iwa.egg-info/requires.txt +0 -0
  170. {iwa-0.0.21 → iwa-0.0.24}/src/iwa.egg-info/top_level.txt +0 -0
  171. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_cow.py +0 -0
  172. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_safe.py +0 -0
  173. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_transaction_retry_logic.py +0 -0
  174. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_tui.py +0 -0
  175. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_wallets_screen.py +0 -0
  176. {iwa-0.0.21 → iwa-0.0.24}/src/tests/legacy_web.py +0 -0
  177. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_account_service.py +0 -0
  178. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_balance_service.py +0 -0
  179. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_chain.py +0 -0
  180. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_chain_interface.py +0 -0
  181. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_chain_interface_coverage.py +0 -0
  182. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_cli.py +0 -0
  183. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_contract.py +0 -0
  184. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_db.py +0 -0
  185. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_drain_coverage.py +0 -0
  186. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_erc20.py +0 -0
  187. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_gnosis_plugin.py +0 -0
  188. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_keys.py +0 -0
  189. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_legacy_wallet.py +0 -0
  190. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_main.py +0 -0
  191. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_migration.py +0 -0
  192. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_mnemonic.py +0 -0
  193. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_modals.py +0 -0
  194. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_models.py +0 -0
  195. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_monitor.py +0 -0
  196. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_multisend.py +0 -0
  197. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_plugin_service.py +0 -0
  198. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_pricing.py +0 -0
  199. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_rate_limiter.py +0 -0
  200. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_reset_tenderly.py +0 -0
  201. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_rpc_efficiency.py +0 -0
  202. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_rpc_rotation.py +0 -0
  203. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_rpc_view.py +0 -0
  204. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_safe_coverage.py +0 -0
  205. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_safe_service.py +0 -0
  206. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_service_manager_integration.py +0 -0
  207. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_service_manager_structure.py +0 -0
  208. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_service_transaction.py +0 -0
  209. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_staking_router.py +0 -0
  210. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_staking_simple.py +0 -0
  211. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_tables.py +0 -0
  212. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transaction_service.py +0 -0
  213. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transfer_multisend.py +0 -0
  214. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transfer_native.py +0 -0
  215. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transfer_security.py +0 -0
  216. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transfer_structure.py +0 -0
  217. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_transfer_swap_unit.py +0 -0
  218. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_ui_coverage.py +0 -0
  219. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_utils.py +0 -0
  220. {iwa-0.0.21 → iwa-0.0.24}/src/tests/test_workers.py +0 -0
  221. {iwa-0.0.21 → iwa-0.0.24}/src/tools/create_and_stake_service.py +0 -0
  222. {iwa-0.0.21 → iwa-0.0.24}/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.21
3
+ Version: 0.0.24
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.21"
3
+ version = "0.0.24"
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"
@@ -71,7 +71,7 @@ where = ["src"]
71
71
 
72
72
  [tool.ruff]
73
73
  line-length = 100
74
- target-version = "0.0.21"
74
+ target-version = "0.0.24"
75
75
  fix = true
76
76
 
77
77
  [tool.ruff.lint]
@@ -7,6 +7,7 @@ Supports two formats:
7
7
  """
8
8
 
9
9
  import json
10
+ import re
10
11
  from dataclasses import dataclass, field
11
12
  from pathlib import Path
12
13
  from typing import List, Optional, Tuple
@@ -17,6 +18,28 @@ from loguru import logger
17
18
  from iwa.core.keys import EncryptedAccount, KeyStorage
18
19
  from iwa.core.models import Config, StoredSafeAccount
19
20
 
21
+ # Known mappings from olas-operate-middleware staking programs
22
+ # See: https://github.com/valory-xyz/olas-operate-middleware/blob/main/operate/ledger/profiles.py
23
+ STAKING_PROGRAM_MAP = {
24
+ # Pearl staking programs (gnosis) - operate format
25
+ "pearl_alpha": "0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e", # Expert Legacy
26
+ "pearl_beta": "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C", # Hobbyist 1 Legacy
27
+ "pearl_beta_2": "0xE56dF1E563De1B10715cB313D514af350D207212", # Expert 5 Legacy
28
+ "pearl_beta_3": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
29
+ "pearl_beta_4": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
30
+ "pearl_beta_5": "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f", # Expert 10 Legacy
31
+ "pearl_beta_mm_v2_1": "0x75eeca6207be98cac3fde8a20ecd7b01e50b3472", # Expert 3 MM v2
32
+ "pearl_beta_mm_v2_2": "0x9c7f6103e3a72e4d1805b9c683ea5b370ec1a99f", # Expert 4 MM v2
33
+ "pearl_beta_mm_v2_3": "0xcdC603e0Ee55Aae92519f9770f214b2Be4967f7d", # Expert 5 MM v2
34
+ # Quickstart staking programs (gnosis) - quickstart format
35
+ "quickstart_beta_expert_4": "0xaD9d891134443B443D7F30013c7e14Fe27F2E029", # Expert 4 Legacy
36
+ "quickstart_beta_expert_7": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
37
+ "quickstart_beta_expert_9": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
38
+ "quickstart_beta_expert_11": "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7", # Expert 11 Legacy
39
+ "quickstart_beta_expert_16_mech_marketplace": "0x6c65430515c70a3f5E62107CC301685B7D46f991", # Expert 16 MM v1
40
+ "quickstart_beta_expert_18_mech_marketplace": "0x041e679d04Fc0D4f75Eb937Dea729Df09a58e454", # Expert 18 MM v1
41
+ }
42
+
20
43
 
21
44
  @dataclass
22
45
  class DiscoveredKey:
@@ -26,8 +49,10 @@ class DiscoveredKey:
26
49
  private_key: Optional[str] = None # Plaintext hex (None if still encrypted)
27
50
  encrypted_keystore: Optional[dict] = None # Web3 v3 keystore format
28
51
  source_file: Path = field(default_factory=Path)
29
- role: str = "unknown" # "agent", "operator", "owner"
52
+ role: str = "unknown" # "agent", "owner"
30
53
  is_encrypted: bool = False
54
+ signature_verified: bool = False
55
+ signature_failed: bool = False
31
56
 
32
57
  @property
33
58
  def is_decrypted(self) -> bool:
@@ -55,6 +80,9 @@ class DiscoveredService:
55
80
  source_folder: Path = field(default_factory=Path)
56
81
  format: str = "unknown" # "trader_runner" or "operate"
57
82
  service_name: Optional[str] = None
83
+ # New fields for full service import
84
+ staking_contract_address: Optional[str] = None
85
+ service_owner_address: Optional[str] = None
58
86
 
59
87
  @property
60
88
  def agent_key(self) -> Optional[DiscoveredKey]:
@@ -66,9 +94,14 @@ class DiscoveredService:
66
94
 
67
95
  @property
68
96
  def operator_key(self) -> Optional[DiscoveredKey]:
69
- """Get the operator key if present."""
97
+ """Get the operator (owner) key. Alias for compatibility."""
98
+ return self.owner_key
99
+
100
+ @property
101
+ def owner_key(self) -> Optional[DiscoveredKey]:
102
+ """Get the owner key if present (matches 'owner' or 'operator' roles)."""
70
103
  for key in self.keys:
71
- if key.role in ("operator", "owner"):
104
+ if key.role in ["owner", "operator"]:
72
105
  return key
73
106
  return None
74
107
 
@@ -89,15 +122,17 @@ class ImportResult:
89
122
  class OlasServiceImporter:
90
123
  """Discover and import Olas services from external directories."""
91
124
 
92
- def __init__(self, key_storage: Optional[KeyStorage] = None):
125
+ def __init__(self, key_storage: Optional[KeyStorage] = None, password: Optional[str] = None):
93
126
  """Initialize the importer.
94
127
 
95
128
  Args:
96
129
  key_storage: KeyStorage instance. If None, will create one.
130
+ password: Optional password to decrypt discovered keystores.
97
131
 
98
132
  """
99
133
  self.key_storage = key_storage or KeyStorage()
100
134
  self.config = Config()
135
+ self.password = password
101
136
 
102
137
  def scan_directory(self, path: Path) -> List[DiscoveredService]:
103
138
  """Recursively scan a directory for Olas services.
@@ -106,7 +141,7 @@ class OlasServiceImporter:
106
141
  path: Directory to scan.
107
142
 
108
143
  Returns:
109
- List of discovered services.
144
+ List of discovered services (deduplicated by chain:service_id).
110
145
 
111
146
  """
112
147
  path = Path(path)
@@ -129,8 +164,51 @@ class OlasServiceImporter:
129
164
  services = self._parse_operate_format(operate)
130
165
  discovered.extend(services)
131
166
 
132
- logger.info(f"Discovered {len(discovered)} Olas service(s)")
133
- return discovered
167
+ return self._deduplicate_services(discovered)
168
+
169
+ def _deduplicate_services(self, services: List[DiscoveredService]) -> List[DiscoveredService]:
170
+ """Deduplicate discovered services by chain:service_id."""
171
+ seen_keys: set = set()
172
+ unique_services = []
173
+ duplicates = 0
174
+ for service in services:
175
+ if service.service_id:
176
+ key = f"{service.chain_name}:{service.service_id}"
177
+ if key in seen_keys:
178
+ logger.debug(
179
+ f"Skipping duplicate service {key} from {service.source_folder}"
180
+ )
181
+ duplicates += 1
182
+ continue
183
+ seen_keys.add(key)
184
+ unique_services.append(service)
185
+
186
+ if duplicates:
187
+ logger.info(f"Skipped {duplicates} duplicate service(s)")
188
+ logger.info(f"Discovered {len(unique_services)} unique Olas service(s)")
189
+ return unique_services
190
+
191
+ def _find_trader_name(self, folder: Path) -> str:
192
+ """Find the trader name by traversing up the directory tree.
193
+
194
+ Handles quickstart format where the .operate folder is nested inside
195
+ a quickstart folder, e.g.: trader_altair/quickstart/.operate/
196
+
197
+ Returns the first folder name starting with 'trader_' or the
198
+ immediate folder name if none found.
199
+ """
200
+ current = folder
201
+ fallback = folder.name
202
+
203
+ # Traverse up looking for trader_* folder
204
+ for _ in range(5): # Max 5 levels up
205
+ if current.name.startswith("trader_"):
206
+ return current.name
207
+ current = current.parent
208
+ if current == current.parent: # Reached root
209
+ break
210
+
211
+ return fallback
134
212
 
135
213
  def _parse_trader_runner_format(self, folder: Path) -> Optional[DiscoveredService]:
136
214
  """Parse a .trader_runner folder.
@@ -153,6 +231,9 @@ class OlasServiceImporter:
153
231
  service.safe_address = self._extract_safe_address(folder)
154
232
  service.keys = self._extract_trader_keys(folder)
155
233
 
234
+ # Extract staking program from .env
235
+ self._extract_staking_from_env(service, folder)
236
+
156
237
  if not service.keys and not service.service_id:
157
238
  logger.debug(f"No valid data found in {folder}")
158
239
  return None
@@ -187,10 +268,10 @@ class OlasServiceImporter:
187
268
  if key:
188
269
  keys.append(key)
189
270
 
190
- # Parse operator_pkey.txt
271
+ # Parse operator_pkey.txt (contains owner key)
191
272
  operator_file = folder / "operator_pkey.txt"
192
273
  if operator_file.exists():
193
- key = self._parse_keystore_file(operator_file, role="operator")
274
+ key = self._parse_keystore_file(operator_file, role="owner")
194
275
  if key:
195
276
  keys.append(key)
196
277
 
@@ -205,6 +286,31 @@ class OlasServiceImporter:
205
286
  keys.append(key)
206
287
  return keys
207
288
 
289
+ def _extract_staking_from_env(self, service: DiscoveredService, folder: Path) -> None:
290
+ """Extract STAKING_PROGRAM from .env file in trader_runner folder."""
291
+ # Check parent folder for .env (usually alongside .trader_runner)
292
+ env_file = folder.parent / ".env"
293
+ if not env_file.exists():
294
+ # Also check inside the folder itself
295
+ env_file = folder / ".env"
296
+ if not env_file.exists():
297
+ return
298
+
299
+ try:
300
+ content = env_file.read_text()
301
+ for line in content.splitlines():
302
+ line = line.strip()
303
+ if line.startswith("STAKING_PROGRAM="):
304
+ program_id = line.split("=", 1)[1].strip().strip('"').strip("'")
305
+ if program_id:
306
+ service.staking_contract_address = self._resolve_staking_contract(
307
+ program_id, service.chain_name
308
+ )
309
+ logger.debug(f"Found STAKING_PROGRAM={program_id} in {env_file}")
310
+ break
311
+ except IOError as e:
312
+ logger.warning(f"Failed to read {env_file}: {e}")
313
+
208
314
  def _parse_operate_format(self, folder: Path) -> List[DiscoveredService]:
209
315
  """Parse a .operate folder.
210
316
 
@@ -288,12 +394,15 @@ class OlasServiceImporter:
288
394
 
289
395
  # Use the folder name containing .operate (e.g., "trader_xi")
290
396
  operate_folder = config_file.parent.parent.parent # services/<uuid> -> .operate
291
- parent_folder = operate_folder.parent # .operate -> trader_xi
397
+ parent_folder = operate_folder.parent # .operate -> trader_xi or quickstart
398
+
399
+ # Handle quickstart format: traverse up to find trader_* folder
400
+ service_name = self._find_trader_name(parent_folder)
292
401
 
293
402
  service = DiscoveredService(
294
403
  source_folder=config_file.parent,
295
404
  format="operate",
296
- service_name=parent_folder.name,
405
+ service_name=service_name,
297
406
  )
298
407
 
299
408
  # 1. Extract keys from config
@@ -311,6 +420,9 @@ class OlasServiceImporter:
311
420
  external_keys = self._extract_external_keys_folder(operate_folder)
312
421
  self._merge_unique_keys(service, external_keys)
313
422
 
423
+ # 5. Extract owner address from wallets folder
424
+ self._extract_owner_address(service, operate_folder)
425
+
314
426
  return service
315
427
 
316
428
  def _extract_keys_from_operate_config(
@@ -325,19 +437,19 @@ class OlasServiceImporter:
325
437
  # Remove 0x prefix if present
326
438
  if private_key.startswith("0x"):
327
439
  private_key = private_key[2:]
328
- keys.append(
329
- DiscoveredKey(
330
- address=key_data["address"],
331
- private_key=private_key,
332
- role="agent",
333
- source_file=config_file,
334
- is_encrypted=False,
335
- )
440
+ key = DiscoveredKey(
441
+ address=key_data["address"],
442
+ private_key=private_key,
443
+ role="agent",
444
+ source_file=config_file,
445
+ is_encrypted=False,
336
446
  )
447
+ self._verify_key_signature(key)
448
+ keys.append(key)
337
449
  return keys
338
450
 
339
451
  def _enrich_service_with_chain_info(self, service: DiscoveredService, data: dict) -> None:
340
- """Extract service ID and Safe address from chain configs."""
452
+ """Extract service ID, Safe address, and staking contract from chain configs."""
341
453
  chain_configs = data.get("chain_configs", {})
342
454
  for chain_name, chain_config in chain_configs.items():
343
455
  chain_data = chain_config.get("chain_data", {})
@@ -350,6 +462,25 @@ class OlasServiceImporter:
350
462
  if "multisig" in chain_data:
351
463
  service.safe_address = chain_data["multisig"]
352
464
 
465
+ # Extract staking contract from user_params
466
+ user_params = chain_data.get("user_params", {})
467
+ staking_program_id = user_params.get("staking_program_id")
468
+ if staking_program_id:
469
+ service.staking_contract_address = self._resolve_staking_contract(
470
+ staking_program_id, chain_name
471
+ )
472
+
473
+ def _resolve_staking_contract(
474
+ self, staking_program_id: str, chain_name: str
475
+ ) -> Optional[str]:
476
+ """Resolve a staking program ID to a contract address."""
477
+ address = STAKING_PROGRAM_MAP.get(staking_program_id)
478
+ if address:
479
+ logger.debug(f"Resolved staking program '{staking_program_id}' -> {address}")
480
+ else:
481
+ logger.warning(f"Unknown staking program ID: {staking_program_id}")
482
+ return address
483
+
353
484
  def _extract_parent_wallet_keys(self, operate_folder: Path) -> List[DiscoveredKey]:
354
485
  """Extract owner keys from parent wallets folder."""
355
486
  keys = []
@@ -357,7 +488,12 @@ class OlasServiceImporter:
357
488
  if wallets_folder.exists():
358
489
  eth_txt = wallets_folder / "ethereum.txt"
359
490
  if eth_txt.exists():
491
+ # Try plaintext first
360
492
  key = self._parse_plaintext_key_file(eth_txt, role="owner")
493
+ if not key:
494
+ # Fallback to keystore
495
+ key = self._parse_keystore_file(eth_txt, role="owner")
496
+
361
497
  if key:
362
498
  keys.append(key)
363
499
  return keys
@@ -374,6 +510,22 @@ class OlasServiceImporter:
374
510
  keys.append(key)
375
511
  return keys
376
512
 
513
+ def _extract_owner_address(self, service: DiscoveredService, operate_folder: Path) -> None:
514
+ """Extract owner address from wallets/ethereum.json."""
515
+ wallets_folder = operate_folder / "wallets"
516
+ if not wallets_folder.exists():
517
+ return
518
+
519
+ eth_json = wallets_folder / "ethereum.json"
520
+ if eth_json.exists():
521
+ try:
522
+ data = json.loads(eth_json.read_text())
523
+ if "address" in data:
524
+ service.service_owner_address = data["address"]
525
+ logger.debug(f"Extracted owner address: {service.service_owner_address}")
526
+ except (json.JSONDecodeError, IOError) as e:
527
+ logger.warning(f"Failed to parse {eth_json}: {e}")
528
+
377
529
  def _merge_unique_keys(self, service: DiscoveredService, new_keys: List[DiscoveredKey]):
378
530
  """Merge new keys into service avoiding duplicates by address."""
379
531
  existing_addrs = {k.address.lower() for k in service.keys}
@@ -398,13 +550,21 @@ class OlasServiceImporter:
398
550
  if not address.startswith("0x"):
399
551
  address = "0x" + address
400
552
 
401
- return DiscoveredKey(
553
+ key = DiscoveredKey(
402
554
  address=address,
403
555
  encrypted_keystore=keystore,
404
556
  role=role,
405
557
  source_file=file_path,
406
558
  is_encrypted=True,
407
559
  )
560
+
561
+ # Attempt decryption if password provided
562
+ if self.password:
563
+ self._attempt_decryption(key)
564
+ if key.private_key:
565
+ self._verify_key_signature(key)
566
+
567
+ return key
408
568
  except (json.JSONDecodeError, IOError) as e:
409
569
  logger.warning(f"Failed to parse keystore {file_path}: {e}")
410
570
  return None
@@ -422,15 +582,19 @@ class OlasServiceImporter:
422
582
  address = keystore.get("address", "")
423
583
  if not address.startswith("0x"):
424
584
  address = "0x" + address
425
- keys.append(
426
- DiscoveredKey(
427
- address=address,
428
- encrypted_keystore=keystore,
429
- role="agent",
430
- source_file=file_path,
431
- is_encrypted=True,
432
- )
585
+ key = DiscoveredKey(
586
+ address=address,
587
+ encrypted_keystore=keystore,
588
+ role="agent",
589
+ source_file=file_path,
590
+ is_encrypted=True,
433
591
  )
592
+ # Attempt decryption if password provided
593
+ if self.password:
594
+ self._attempt_decryption(key)
595
+ if key.private_key:
596
+ self._verify_key_signature(key)
597
+ keys.append(key)
434
598
  return keys
435
599
  except (json.JSONDecodeError, IOError):
436
600
  return []
@@ -446,13 +610,15 @@ class OlasServiceImporter:
446
610
  try:
447
611
  data = json.loads(content)
448
612
  if isinstance(data, dict) and "private_key" in data and "address" in data:
449
- return DiscoveredKey(
613
+ key = DiscoveredKey(
450
614
  address=data["address"],
451
615
  private_key=data["private_key"],
452
616
  role=role,
453
617
  source_file=file_path,
454
618
  is_encrypted=False,
455
619
  )
620
+ self._verify_key_signature(key)
621
+ return key
456
622
  except json.JSONDecodeError:
457
623
  pass
458
624
 
@@ -460,13 +626,15 @@ class OlasServiceImporter:
460
626
  if len(content) == 64 or (len(content) == 66 and content.startswith("0x")):
461
627
  private_key = content[2:] if content.startswith("0x") else content
462
628
  account = Account.from_key(bytes.fromhex(private_key))
463
- return DiscoveredKey(
629
+ key = DiscoveredKey(
464
630
  address=account.address,
465
631
  private_key=private_key,
466
632
  role=role,
467
633
  source_file=file_path,
468
634
  is_encrypted=False,
469
635
  )
636
+ self._verify_key_signature(key)
637
+ return key
470
638
 
471
639
  return None
472
640
  except Exception as e:
@@ -625,8 +793,6 @@ class OlasServiceImporter:
625
793
  Tags follow the pattern: {service_name}_{role}
626
794
  Example: trader_alpha_agent, trader_alpha_operator
627
795
  """
628
- import re
629
-
630
796
  # Use service name as prefix, or 'imported' as fallback
631
797
  prefix = service_name or "imported"
632
798
 
@@ -667,7 +833,6 @@ class OlasServiceImporter:
667
833
  signers.append(key.address)
668
834
 
669
835
  # Generate tag
670
- import re
671
836
 
672
837
  prefix = service.service_name or "imported"
673
838
  prefix = re.sub(r"[^a-z0-9]+", "_", prefix.lower()).strip("_")
@@ -698,6 +863,7 @@ class OlasServiceImporter:
698
863
  def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
699
864
  """Import service config to OlasConfig."""
700
865
  try:
866
+ from iwa.plugins.olas.constants import OLAS_TOKEN_ADDRESS_GNOSIS
701
867
  from iwa.plugins.olas.models import OlasConfig, Service
702
868
 
703
869
  # Get or create OlasConfig
@@ -711,13 +877,16 @@ class OlasServiceImporter:
711
877
  if key in olas_config.services:
712
878
  return False, "duplicate"
713
879
 
714
- # Create service model
880
+ # Create service model with all fields
715
881
  olas_service = Service(
716
882
  service_name=service.service_name or f"service_{service.service_id}",
717
883
  chain_name=service.chain_name,
718
884
  service_id=service.service_id,
719
- agent_ids=[], # Would need on-chain query
885
+ agent_ids=[25], # Trader agents always use agent ID 25
720
886
  multisig_address=service.safe_address,
887
+ service_owner_address=service.service_owner_address,
888
+ staking_contract_address=service.staking_contract_address,
889
+ token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
721
890
  )
722
891
 
723
892
  # Set agent address if we have one
@@ -726,7 +895,7 @@ class OlasServiceImporter:
726
895
  olas_service.agent_address = agent_key.address
727
896
 
728
897
  olas_config.add_service(olas_service)
729
- self.config.save()
898
+ self.config.save_config()
730
899
  logger.info(f"Imported service {key}")
731
900
  return True, "ok"
732
901
 
@@ -734,3 +903,58 @@ class OlasServiceImporter:
734
903
  return False, "Olas plugin not available"
735
904
  except Exception as e:
736
905
  return False, str(e)
906
+
907
+ def _attempt_decryption(self, key: DiscoveredKey) -> None:
908
+ """Attempt to decrypt an encrypted keystore using the provided password."""
909
+ if not self.password or not key.encrypted_keystore:
910
+ return
911
+
912
+ try:
913
+ logger.debug(f"Attempting decryption for {key.address}")
914
+
915
+ # Use Account.decrypt to handle standard web3 keystores
916
+ private_key_bytes = Account.decrypt(key.encrypted_keystore, self.password)
917
+ key.private_key = private_key_bytes.hex()
918
+ key.is_encrypted = False
919
+ # If we successfully decrypted, it's no longer "encrypted" for verification purposes
920
+ logger.debug(f"Successfully decrypted key for {key.address}")
921
+ except ValueError as e:
922
+ # Password incorrect
923
+ logger.warning(f"Decryption failed (ValueError) for {key.address}: {e}")
924
+ except Exception as e:
925
+ logger.warning(f"Error decrypting key {key.address}: {type(e).__name__} - {e}")
926
+
927
+ def _verify_key_signature(self, key: DiscoveredKey) -> None:
928
+ """Verify that the plaintext private key can sign a message and recover the address."""
929
+ if not key.private_key or not key.address:
930
+ return
931
+
932
+ try:
933
+ from eth_account.messages import encode_defunct
934
+
935
+ message = "Hello, world!"
936
+ encoded_message = encode_defunct(text=message)
937
+ signed_message = Account.sign_message(encoded_message, private_key=key.private_key)
938
+ recovered_address = Account.recover_message(
939
+ encoded_message, signature=signed_message.signature
940
+ )
941
+
942
+ # Normalize address to lowercase with 0x prefix
943
+ key_addr = key.address.lower()
944
+ if not key_addr.startswith("0x"):
945
+ key_addr = "0x" + key_addr
946
+ recovered_addr = recovered_address.lower()
947
+
948
+ if recovered_addr == key_addr:
949
+ key.signature_verified = True
950
+ logger.debug(f"Signature verified for key {key.address}")
951
+ else:
952
+ key.signature_failed = True
953
+ logger.warning(
954
+ f"Signature verification FAILED for key {key.address}. "
955
+ f"Recovered: {recovered_address}"
956
+ )
957
+ except Exception as e:
958
+ key.signature_failed = True
959
+ logger.warning(f"Error verifying signature for key {key.address}: {e}")
960
+