iwa 0.0.27__tar.gz → 0.0.28__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 (223) hide show
  1. {iwa-0.0.27/src/iwa.egg-info → iwa-0.0.28}/PKG-INFO +1 -1
  2. {iwa-0.0.27 → iwa-0.0.28}/pyproject.toml +2 -2
  3. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/cli.py +18 -0
  4. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/contract.py +12 -1
  5. iwa-0.0.28/src/iwa/core/contracts/decoder.py +154 -0
  6. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transaction.py +63 -2
  7. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/wallet.py +1 -1
  8. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/importer.py +105 -29
  9. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/models.py +33 -2
  10. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/plugin.py +68 -17
  11. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +5 -17
  12. {iwa-0.0.27 → iwa-0.0.28/src/iwa.egg-info}/PKG-INFO +1 -1
  13. {iwa-0.0.27 → iwa-0.0.28}/src/iwa.egg-info/SOURCES.txt +1 -0
  14. {iwa-0.0.27 → iwa-0.0.28}/LICENSE +0 -0
  15. {iwa-0.0.27 → iwa-0.0.28}/README.md +0 -0
  16. {iwa-0.0.27 → iwa-0.0.28}/setup.cfg +0 -0
  17. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/__init__.py +0 -0
  18. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/__main__.py +0 -0
  19. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/__init__.py +0 -0
  20. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/__init__.py +0 -0
  21. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/errors.py +0 -0
  22. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/interface.py +0 -0
  23. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/manager.py +0 -0
  24. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/models.py +0 -0
  25. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chain/rate_limiter.py +0 -0
  26. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/chainlist.py +0 -0
  27. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/constants.py +0 -0
  28. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/__init__.py +0 -0
  29. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/abis/erc20.json +0 -0
  30. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/abis/multisend.json +0 -0
  31. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
  32. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/cache.py +0 -0
  33. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/erc20.py +0 -0
  34. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/contracts/multisend.py +0 -0
  35. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/db.py +0 -0
  36. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/ipfs.py +0 -0
  37. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/keys.py +0 -0
  38. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/mnemonic.py +0 -0
  39. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/models.py +0 -0
  40. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/monitor.py +0 -0
  41. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/plugins.py +0 -0
  42. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/pricing.py +0 -0
  43. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/rpc_monitor.py +0 -0
  44. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/secrets.py +0 -0
  45. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/__init__.py +0 -0
  46. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/account.py +0 -0
  47. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/balance.py +0 -0
  48. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/plugin.py +0 -0
  49. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/safe.py +0 -0
  50. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/__init__.py +0 -0
  51. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/base.py +0 -0
  52. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/erc20.py +0 -0
  53. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/multisend.py +0 -0
  54. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/native.py +0 -0
  55. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/services/transfer/swap.py +0 -0
  56. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/tables.py +0 -0
  57. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/test.py +0 -0
  58. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/tests/test_wallet.py +0 -0
  59. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/types.py +0 -0
  60. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/ui.py +0 -0
  61. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/core/utils.py +0 -0
  62. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/__init__.py +0 -0
  63. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/__init__.py +0 -0
  64. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
  65. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
  66. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
  67. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/cow/types.py +0 -0
  68. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
  69. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/plugin.py +0 -0
  70. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/safe.py +0 -0
  71. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
  72. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
  73. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/__init__.py +0 -0
  74. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/constants.py +0 -0
  75. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
  76. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
  77. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
  78. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
  79. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
  80. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
  81. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
  82. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
  83. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
  84. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
  85. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
  86. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/base.py +0 -0
  87. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/mech.py +0 -0
  88. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
  89. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
  90. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/service.py +0 -0
  91. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/contracts/staking.py +0 -0
  92. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/events.py +0 -0
  93. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/mech_reference.py +0 -0
  94. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
  95. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
  96. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
  97. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/base.py +0 -0
  98. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
  99. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
  100. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
  101. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
  102. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/conftest.py +0 -0
  103. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
  104. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
  105. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
  106. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
  107. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
  108. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
  109. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
  110. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
  111. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
  112. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
  113. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
  114. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
  115. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
  116. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
  117. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
  118. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
  119. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
  120. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
  121. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
  122. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
  123. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tui/__init__.py +0 -0
  124. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
  125. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/__init__.py +0 -0
  126. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/check_profile.py +0 -0
  127. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/list_contracts.py +0 -0
  128. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/release.py +0 -0
  129. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/reset_env.py +0 -0
  130. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/reset_tenderly.py +0 -0
  131. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/restore_backup.py +0 -0
  132. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/test_chainlist.py +0 -0
  133. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tools/wallet_check.py +0 -0
  134. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/__init__.py +0 -0
  135. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/app.py +0 -0
  136. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/modals/__init__.py +0 -0
  137. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/modals/base.py +0 -0
  138. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/rpc.py +0 -0
  139. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/screens/__init__.py +0 -0
  140. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/screens/wallets.py +0 -0
  141. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/tests/test_app.py +0 -0
  142. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/tests/test_rpc.py +0 -0
  143. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
  144. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/tests/test_widgets.py +0 -0
  145. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/widgets/__init__.py +0 -0
  146. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/widgets/base.py +0 -0
  147. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/tui/workers.py +0 -0
  148. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/dependencies.py +0 -0
  149. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/models.py +0 -0
  150. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/accounts.py +0 -0
  151. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/__init__.py +0 -0
  152. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/admin.py +0 -0
  153. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/funding.py +0 -0
  154. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/general.py +0 -0
  155. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/services.py +0 -0
  156. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/olas/staking.py +0 -0
  157. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/state.py +0 -0
  158. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/swap.py +0 -0
  159. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/routers/transactions.py +0 -0
  160. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/server.py +0 -0
  161. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/static/app.js +0 -0
  162. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/static/index.html +0 -0
  163. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/static/style.css +0 -0
  164. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/tests/test_web_endpoints.py +0 -0
  165. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/tests/test_web_olas.py +0 -0
  166. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/tests/test_web_swap.py +0 -0
  167. {iwa-0.0.27 → iwa-0.0.28}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
  168. {iwa-0.0.27 → iwa-0.0.28}/src/iwa.egg-info/dependency_links.txt +0 -0
  169. {iwa-0.0.27 → iwa-0.0.28}/src/iwa.egg-info/entry_points.txt +0 -0
  170. {iwa-0.0.27 → iwa-0.0.28}/src/iwa.egg-info/requires.txt +0 -0
  171. {iwa-0.0.27 → iwa-0.0.28}/src/iwa.egg-info/top_level.txt +0 -0
  172. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_cow.py +0 -0
  173. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_safe.py +0 -0
  174. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_transaction_retry_logic.py +0 -0
  175. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_tui.py +0 -0
  176. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_wallets_screen.py +0 -0
  177. {iwa-0.0.27 → iwa-0.0.28}/src/tests/legacy_web.py +0 -0
  178. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_account_service.py +0 -0
  179. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_balance_service.py +0 -0
  180. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_chain.py +0 -0
  181. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_chain_interface.py +0 -0
  182. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_chain_interface_coverage.py +0 -0
  183. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_cli.py +0 -0
  184. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_contract.py +0 -0
  185. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_db.py +0 -0
  186. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_drain_coverage.py +0 -0
  187. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_erc20.py +0 -0
  188. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_gnosis_plugin.py +0 -0
  189. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_keys.py +0 -0
  190. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_legacy_wallet.py +0 -0
  191. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_main.py +0 -0
  192. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_migration.py +0 -0
  193. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_mnemonic.py +0 -0
  194. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_modals.py +0 -0
  195. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_models.py +0 -0
  196. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_monitor.py +0 -0
  197. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_multisend.py +0 -0
  198. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_plugin_service.py +0 -0
  199. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_pricing.py +0 -0
  200. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_rate_limiter.py +0 -0
  201. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_reset_tenderly.py +0 -0
  202. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_rpc_efficiency.py +0 -0
  203. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_rpc_rotation.py +0 -0
  204. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_rpc_view.py +0 -0
  205. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_safe_coverage.py +0 -0
  206. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_safe_service.py +0 -0
  207. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_service_manager_integration.py +0 -0
  208. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_service_manager_structure.py +0 -0
  209. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_service_transaction.py +0 -0
  210. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_staking_router.py +0 -0
  211. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_staking_simple.py +0 -0
  212. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_tables.py +0 -0
  213. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transaction_service.py +0 -0
  214. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transfer_multisend.py +0 -0
  215. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transfer_native.py +0 -0
  216. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transfer_security.py +0 -0
  217. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transfer_structure.py +0 -0
  218. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_transfer_swap_unit.py +0 -0
  219. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_ui_coverage.py +0 -0
  220. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_utils.py +0 -0
  221. {iwa-0.0.27 → iwa-0.0.28}/src/tests/test_workers.py +0 -0
  222. {iwa-0.0.27 → iwa-0.0.28}/src/tools/create_and_stake_service.py +0 -0
  223. {iwa-0.0.27 → iwa-0.0.28}/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.27
3
+ Version: 0.0.28
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.27"
3
+ version = "0.0.28"
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.27"
74
+ target-version = "0.0.28"
75
75
  fix = true
76
76
 
77
77
  [tool.ruff.lint]
@@ -7,6 +7,7 @@ from web3 import Web3
7
7
 
8
8
  from iwa.core.chain import ChainInterfaces
9
9
  from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
10
+ from iwa.core.contracts.decoder import ErrorDecoder
10
11
  from iwa.core.keys import KeyStorage
11
12
  from iwa.core.services import PluginService
12
13
  from iwa.core.tables import list_accounts
@@ -206,6 +207,23 @@ def web_server(
206
207
  run_server(host=host, port=server_port)
207
208
 
208
209
 
210
+ @iwa_cli.command("decode")
211
+ def decode_hex(
212
+ hex_data: str = typer.Argument(..., help="The hex-encoded error data (e.g., 0xa43d6ada...)"),
213
+ ):
214
+ """Decode a hex error identifier into a human-readable message."""
215
+ decoder = ErrorDecoder()
216
+ results = decoder.decode(hex_data)
217
+
218
+ if not results:
219
+ typer.echo(f"Could not decode error data: {hex_data}")
220
+ return
221
+
222
+ typer.echo(f"\nDecoding results for {hex_data[:10]}:")
223
+ for _name, msg, source in results:
224
+ typer.echo(f" [{source}] {msg}")
225
+
226
+
209
227
  @wallet_cli.command("drain")
210
228
  def drain_wallet(
211
229
  from_address_or_tag: str = typer.Option(..., "--from", "-f", help="From address or tag"),
@@ -11,6 +11,7 @@ from web3.contract import Contract
11
11
  from web3.exceptions import ContractCustomError
12
12
 
13
13
  from iwa.core.chain import ChainInterfaces
14
+ from iwa.core.contracts.decoder import ErrorDecoder
14
15
  from iwa.core.rpc_monitor import RPCMonitor
15
16
  from iwa.core.utils import configure_logger
16
17
 
@@ -111,7 +112,7 @@ class ContractInstance:
111
112
  )
112
113
  return selectors
113
114
 
114
- def decode_error(self, error_data: str) -> Optional[Tuple[str, str]]:
115
+ def decode_error(self, error_data: str) -> Optional[Tuple[str, str]]: # noqa: C901
115
116
  """Decode error data from a failed transaction or call.
116
117
 
117
118
  Handles:
@@ -172,6 +173,16 @@ class ContractInstance:
172
173
  logger.debug(f"Failed to decode Panic(uint256): {e}")
173
174
  return ("Panic", "Failed to decode panic code")
174
175
 
176
+ # 4. Global Fallback Decoder
177
+ try:
178
+ global_results = ErrorDecoder().decode(error_data)
179
+ if global_results:
180
+ # Use the first match
181
+ error_name, error_msg, _ = global_results[0]
182
+ return (error_name, error_msg)
183
+ except Exception as e:
184
+ logger.debug(f"Global decoder failed: {e}")
185
+
175
186
  return None
176
187
 
177
188
  def _extract_error_data(self, exception: Exception) -> Optional[str]:
@@ -0,0 +1,154 @@
1
+ """Global error decoder for Ethereum contracts."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Tuple
6
+
7
+ from eth_abi import decode
8
+ from loguru import logger
9
+ from web3 import Web3
10
+
11
+ # Standard error selectors (copied from contract.py for consistency)
12
+ ERROR_SELECTOR = "0x08c379a0" # Error(string)
13
+ PANIC_SELECTOR = "0x4e487b71" # Panic(uint256)
14
+
15
+ PANIC_CODES = {
16
+ 0x00: "Generic compiler inserted panic",
17
+ 0x01: "Assert failed",
18
+ 0x11: "Arithmetic overflow/underflow",
19
+ 0x12: "Division by zero",
20
+ 0x21: "Invalid enum value",
21
+ 0x22: "Storage byte array incorrectly encoded",
22
+ 0x31: "Pop on empty array",
23
+ 0x32: "Array index out of bounds",
24
+ 0x41: "Too much memory allocated",
25
+ 0x51: "Invalid internal function call",
26
+ }
27
+
28
+
29
+ class ErrorDecoder:
30
+ """Global registry of error selectors from all project ABIs."""
31
+
32
+ _instance = None
33
+ _selectors: Dict[str, List[Dict[str, Any]]] = {} # selector -> list of possible decodings
34
+ _initialized = False
35
+
36
+ def __new__(cls):
37
+ """Singleton pattern."""
38
+ if cls._instance is None:
39
+ cls._instance = super(ErrorDecoder, cls).__new__(cls)
40
+ return cls._instance
41
+
42
+ def __init__(self):
43
+ """Initialize and load all ABIs once."""
44
+ if self._initialized:
45
+ return
46
+ self.load_all_abis()
47
+ self._initialized = True
48
+
49
+ def load_all_abis(self):
50
+ """Find and load all ABI files in the project."""
51
+ # Find the root of the source tree
52
+ # Assuming we are in src/iwa/core/contracts/decoder.py
53
+ current_file = Path(__file__).resolve()
54
+ src_root = current_file.parents[3] # Go up to 'src'
55
+
56
+ abi_files = list(src_root.glob("**/contracts/abis/*.json"))
57
+
58
+ # Also check core ABIs if they are in a different place
59
+ core_abi_path = src_root / "iwa" / "core" / "contracts" / "abis"
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")))
62
+
63
+ logger.debug(f"Found {len(abi_files)} ABI files for error decoding.")
64
+
65
+ for abi_path in abi_files:
66
+ try:
67
+ with open(abi_path, "r", encoding="utf-8") as f:
68
+ content = json.load(f)
69
+ abi = content.get("abi") if isinstance(content, dict) and "abi" in content else content
70
+ if isinstance(abi, list):
71
+ self._process_abi(abi, abi_path.name)
72
+ except Exception as e:
73
+ logger.warning(f"Failed to load ABI {abi_path}: {e}")
74
+
75
+ def _process_abi(self, abi: List[Dict], source_name: str):
76
+ """Extract error selectors from an ABI."""
77
+ for entry in abi:
78
+ if entry.get("type") == "error":
79
+ name = entry["name"]
80
+ inputs = entry.get("inputs", [])
81
+ types = [i["type"] for i in inputs]
82
+ names = [i["name"] for i in inputs]
83
+
84
+ # Signature: Name(type1,type2,...)
85
+ types_str = ",".join(types)
86
+ signature = f"{name}({types_str})"
87
+ selector = "0x" + Web3.keccak(text=signature)[:4].hex()
88
+
89
+ decoding = {
90
+ "name": name,
91
+ "types": types,
92
+ "arg_names": names,
93
+ "source": source_name,
94
+ "signature": signature
95
+ }
96
+
97
+ if selector not in self._selectors:
98
+ self._selectors[selector] = []
99
+
100
+ # Avoid duplicates
101
+ if decoding not in self._selectors[selector]:
102
+ self._selectors[selector].append(decoding)
103
+
104
+ def decode(self, error_data: str) -> List[Tuple[str, str, str]]: # noqa: C901
105
+ """Decode hex error data.
106
+
107
+ Returns:
108
+ List of (error_name, formatted_message, source_abi)
109
+
110
+ """
111
+ if not error_data:
112
+ return []
113
+
114
+ if not error_data.startswith("0x"):
115
+ error_data = "0x" + error_data
116
+
117
+ if len(error_data) < 10:
118
+ return []
119
+
120
+ selector = error_data[:10].lower()
121
+ encoded_args = error_data[10:]
122
+
123
+ results = []
124
+
125
+ # 1. Check Standard Error(string)
126
+ if selector == ERROR_SELECTOR:
127
+ try:
128
+ decoded = decode(["string"], bytes.fromhex(encoded_args))
129
+ results.append(("Error", f"Error: {decoded[0]}", "Built-in"))
130
+ except Exception:
131
+ pass
132
+
133
+ # 2. Check Panic(uint256)
134
+ if selector == PANIC_SELECTOR:
135
+ try:
136
+ decoded = decode(["uint256"], bytes.fromhex(encoded_args))
137
+ code = decoded[0]
138
+ msg = PANIC_CODES.get(code, f"Unknown panic code {code}")
139
+ results.append(("Panic", f"Panic: {msg}", "Built-in"))
140
+ except Exception:
141
+ pass
142
+
143
+ # 3. Check Custom Errors
144
+ if selector in self._selectors:
145
+ for d in self._selectors[selector]:
146
+ try:
147
+ 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))
149
+ results.append((d["name"], f"{d['name']}({args_str})", d["source"]))
150
+ except Exception:
151
+ # Try next possible decoding for this selector
152
+ continue
153
+
154
+ return results
@@ -9,11 +9,14 @@ from web3 import exceptions as web3_exceptions
9
9
  from iwa.core.chain import ChainInterfaces
10
10
  from iwa.core.db import log_transaction
11
11
  from iwa.core.keys import KeyStorage
12
+ from iwa.core.models import StoredSafeAccount
12
13
  from iwa.core.services.account import AccountService
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from iwa.core.chain import ChainInterface
16
17
 
18
+ # Circular import during type checking
19
+
17
20
  # ERC20 Transfer event signature: Transfer(address indexed from, address indexed to, uint256 value)
18
21
  TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
19
22
 
@@ -205,12 +208,13 @@ class TransferLogger:
205
208
  class TransactionService:
206
209
  """Manages transaction lifecycle: signing, sending, retrying."""
207
210
 
208
- def __init__(self, key_storage: KeyStorage, account_service: AccountService):
211
+ def __init__(self, key_storage: KeyStorage, account_service: AccountService, safe_service=None):
209
212
  """Initialize TransactionService."""
210
213
  self.key_storage = key_storage
211
214
  self.account_service = account_service
215
+ self.safe_service = safe_service
212
216
 
213
- def sign_and_send(
217
+ def sign_and_send( # noqa: C901
214
218
  self,
215
219
  transaction: dict,
216
220
  signer_address_or_tag: str,
@@ -228,6 +232,14 @@ class TransactionService:
228
232
  if not self._prepare_transaction(tx, signer_address_or_tag, chain_interface):
229
233
  return False, {}
230
234
 
235
+ # CHECK FOR SAFE TRANSACTION
236
+ signer_account = self.account_service.resolve_account(signer_address_or_tag)
237
+ if isinstance(signer_account, StoredSafeAccount):
238
+ if not self.safe_service:
239
+ logger.error("Attempted Safe transaction but SafeService is not initialized.")
240
+ return False, {}
241
+ return self._execute_via_safe(tx, signer_account, chain_interface, chain_name, tags)
242
+
231
243
  # Mutable state for retry attempts
232
244
  state = {"gas_retries": 0, "max_gas_retries": 5}
233
245
 
@@ -367,6 +379,55 @@ class TransactionService:
367
379
 
368
380
  return list(set(final_tags))
369
381
 
382
+ def _execute_via_safe(
383
+ self,
384
+ tx: dict,
385
+ signer_account: StoredSafeAccount,
386
+ chain_interface,
387
+ chain_name: str,
388
+ tags: List[str] = None
389
+ ) -> Tuple[bool, Dict]:
390
+ """Execute transaction via SafeService."""
391
+ logger.info(f"Routing transaction via Safe {signer_account.address}...")
392
+
393
+ try:
394
+ # Extract basic params
395
+ to_addr = tx.get("to")
396
+ value = tx.get("value", 0)
397
+ data = tx.get("data", "")
398
+ if isinstance(data, bytes):
399
+ data = "0x" + data.hex()
400
+
401
+ # Execute
402
+ tx_hash = self.safe_service.execute_safe_transaction(
403
+ safe_address_or_tag=signer_account.address,
404
+ to=to_addr,
405
+ value=value,
406
+ chain_name=chain_name,
407
+ data=data
408
+ )
409
+
410
+ # Wait for receipt
411
+ receipt = chain_interface.web3.eth.wait_for_transaction_receipt(tx_hash)
412
+
413
+ status = getattr(receipt, "status", None)
414
+ if status is None and isinstance(receipt, dict):
415
+ status = receipt.get("status")
416
+
417
+ if receipt and status == 1:
418
+ logger.info(f"Safe transaction executed successfully. Tx Hash: {tx_hash}")
419
+ self._log_successful_transaction(
420
+ receipt, tx, signer_account, chain_name, bytes.fromhex(tx_hash.replace("0x", "")), tags, chain_interface
421
+ )
422
+ return True, receipt
423
+ else:
424
+ logger.error("Safe transaction failed (status 0).")
425
+ return False, {}
426
+
427
+ except Exception as e:
428
+ logger.exception(f"Safe transaction failed: {e}")
429
+ return False, {}
430
+
370
431
  def _is_gas_too_low_error(self, err_text: str) -> bool:
371
432
  """Check if error is due to low gas."""
372
433
  low_gas_signals = [
@@ -37,7 +37,7 @@ class Wallet:
37
37
  self.balance_service = BalanceService(self.key_storage, self.account_service)
38
38
  self.safe_service = SafeService(self.key_storage, self.account_service)
39
39
  # self.transaction_manager = TransactionManager(self.key_storage, self.account_service)
40
- self.transaction_service = TransactionService(self.key_storage, self.account_service)
40
+ self.transaction_service = TransactionService(self.key_storage, self.account_service, self.safe_service)
41
41
 
42
42
  self.transfer_service = TransferService(
43
43
  self.key_storage,
@@ -82,7 +82,13 @@ class DiscoveredService:
82
82
  service_name: Optional[str] = None
83
83
  # New fields for full service import
84
84
  staking_contract_address: Optional[str] = None
85
- service_owner_address: Optional[str] = None
85
+ service_owner_eoa_address: Optional[str] = None
86
+ service_owner_multisig_address: Optional[str] = None
87
+
88
+ @property
89
+ def service_owner_address(self) -> Optional[str]:
90
+ """Backward compatibility: effective owner address."""
91
+ return self.service_owner_multisig_address or self.service_owner_eoa_address
86
92
 
87
93
  @property
88
94
  def agent_key(self) -> Optional[DiscoveredKey]:
@@ -517,7 +523,12 @@ class OlasServiceImporter:
517
523
  return keys
518
524
 
519
525
  def _extract_owner_address(self, service: DiscoveredService, operate_folder: Path) -> None:
520
- """Extract owner address from wallets/ethereum.json."""
526
+ """Extract owner address from wallets/ethereum.json.
527
+
528
+ Handles two cases:
529
+ 1. EOA is the owner (legacy).
530
+ 2. Safe is the owner, and EOA is a signer (new staking programs).
531
+ """
521
532
  wallets_folder = operate_folder / "wallets"
522
533
  if not wallets_folder.exists():
523
534
  return
@@ -526,9 +537,35 @@ class OlasServiceImporter:
526
537
  if eth_json.exists():
527
538
  try:
528
539
  data = json.loads(eth_json.read_text())
529
- if "address" in data:
530
- service.service_owner_address = data["address"]
531
- logger.debug(f"Extracted owner address: {service.service_owner_address}")
540
+
541
+ # Check for "safes" entry which indicates the owner is a Safe
542
+ # Structure: "safes": { "gnosis": "0x..." }
543
+ if "safes" in data and FLAGS_OWNER_SAFE in data["safes"]: # Need to detect chain dynamically or iterate
544
+ pass
545
+
546
+ # Logic update:
547
+ # 1. Capture EOA address always (it's the signer)
548
+ eoa_address = data.get("address")
549
+
550
+ # 2. Check for Safe Owner for the current service chain
551
+ safe_owner_address = None
552
+ if "safes" in data and isinstance(data["safes"], dict):
553
+ # We try to match with service.chain_name if available, usually "gnosis"
554
+ chain = service.chain_name or "gnosis"
555
+ safe_owner_address = data["safes"].get(chain)
556
+
557
+ if safe_owner_address:
558
+ # CASE: Owner is Safe
559
+ service.service_owner_multisig_address = safe_owner_address
560
+ service.service_owner_eoa_address = eoa_address # The EOA is the signer/controller
561
+
562
+ logger.debug(f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})")
563
+ elif eoa_address:
564
+ # CASE: Owner is EOA
565
+ service.service_owner_eoa_address = eoa_address
566
+ service.service_owner_multisig_address = None
567
+ logger.debug(f"Extracted EOA owner address: {eoa_address}")
568
+
532
569
  except (json.JSONDecodeError, IOError) as e:
533
570
  logger.warning(f"Failed to parse {eth_json}: {e}")
534
571
 
@@ -541,14 +578,14 @@ class OlasServiceImporter:
541
578
  existing_addrs.add(key.address.lower())
542
579
 
543
580
  def _infer_owner_address(self, service: DiscoveredService) -> None:
544
- """Infer service_owner_address from keys with role='owner' if not already set."""
545
- if service.service_owner_address:
581
+ """Infer service_owner_eoa_address from keys with role='owner' if not already set."""
582
+ if service.service_owner_eoa_address:
546
583
  return # Already set
547
584
 
548
585
  for key in service.keys:
549
586
  if key.role == "owner" and key.address:
550
- service.service_owner_address = key.address
551
- logger.debug(f"Inferred owner address from key: {key.address}")
587
+ service.service_owner_eoa_address = key.address
588
+ logger.debug(f"Inferred owner EOA address from key: {key.address}")
552
589
  return
553
590
 
554
591
  def _parse_keystore_file(
@@ -725,8 +762,14 @@ class OlasServiceImporter:
725
762
 
726
763
  def _import_discovered_safes(self, service: DiscoveredService, result: ImportResult) -> None:
727
764
  """Import Safe from the service if present."""
765
+ # 1. Import Agent Multisig (the one the agent controls)
728
766
  if service.safe_address:
729
- safe_result = self._import_safe(service)
767
+ safe_result = self._import_safe(
768
+ address=service.safe_address,
769
+ signers=self._get_agent_signers(service),
770
+ tag_suffix="safe", # e.g. trader_zeta_safe
771
+ service_name=service.service_name
772
+ )
730
773
  if safe_result[0]:
731
774
  result.imported_safes.append(service.safe_address)
732
775
  elif safe_result[1] == "duplicate":
@@ -734,6 +777,38 @@ class OlasServiceImporter:
734
777
  else:
735
778
  result.errors.append(f"Safe {service.safe_address}: {safe_result[1]}")
736
779
 
780
+ # 2. Import Owner Safe (if it exists and is different)
781
+ if service.service_owner_multisig_address and service.service_owner_multisig_address != service.safe_address:
782
+ # Signer for Owner Safe is the EOA owner key
783
+ owner_signers = self._get_owner_signers(service)
784
+
785
+ safe_result = self._import_safe(
786
+ address=service.service_owner_multisig_address,
787
+ signers=owner_signers,
788
+ tag_suffix="owner_safe", # e.g. trader_zeta_owner_safe
789
+ service_name=service.service_name
790
+ )
791
+ if safe_result[0]:
792
+ result.imported_safes.append(service.service_owner_multisig_address)
793
+ logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
794
+
795
+ def _get_agent_signers(self, service: DiscoveredService) -> List[str]:
796
+ """Get list of signers for the agent safe."""
797
+ signers = []
798
+ for key in service.keys:
799
+ if key.role == "agent":
800
+ signers.append(key.address)
801
+ return signers
802
+
803
+ def _get_owner_signers(self, service: DiscoveredService) -> List[str]:
804
+ """Get list of signers for the owner safe."""
805
+ signers = []
806
+ for key in service.keys:
807
+ # We look for keys marked as owner/operator
808
+ if key.role in ["owner", "operator"]:
809
+ signers.append(key.address)
810
+ return signers
811
+
737
812
  def _import_discovered_service_config(
738
813
  self, service: DiscoveredService, result: ImportResult
739
814
  ) -> None:
@@ -833,27 +908,26 @@ class OlasServiceImporter:
833
908
  i += 1
834
909
  return f"{base_tag}_{i}"
835
910
 
836
- def _import_safe(self, service: DiscoveredService) -> Tuple[bool, str]:
837
- """Import a Safe from a discovered service."""
838
- if not service.safe_address:
911
+ def _import_safe(
912
+ self,
913
+ address: str,
914
+ signers: List[str] = None,
915
+ tag_suffix: str = "safe",
916
+ service_name: Optional[str] = None
917
+ ) -> Tuple[bool, str]:
918
+ """Import a generic Safe."""
919
+ if not address:
839
920
  return False, "no safe address"
840
921
 
841
922
  # Check for duplicate
842
- existing = self.key_storage.find_stored_account(service.safe_address)
923
+ existing = self.key_storage.find_stored_account(address)
843
924
  if existing:
844
925
  return False, "duplicate"
845
926
 
846
- # Get signers from agent keys
847
- signers = []
848
- for key in service.keys:
849
- if key.role == "agent":
850
- signers.append(key.address)
851
-
852
927
  # Generate tag
853
-
854
- prefix = service.service_name or "imported"
928
+ prefix = service_name or "imported"
855
929
  prefix = re.sub(r"[^a-z0-9]+", "_", prefix.lower()).strip("_")
856
- base_tag = f"{prefix}_safe"
930
+ base_tag = f"{prefix}_{tag_suffix}"
857
931
 
858
932
  existing_tags = {
859
933
  acc.tag for acc in self.key_storage.accounts.values() if hasattr(acc, "tag")
@@ -866,15 +940,15 @@ class OlasServiceImporter:
866
940
 
867
941
  safe_account = StoredSafeAccount(
868
942
  tag=tag,
869
- address=service.safe_address,
870
- chains=[service.chain_name],
943
+ address=address,
944
+ chains=["gnosis"], # TODO: detecting chain dynamically would be better
871
945
  threshold=1, # Default, accurate value requires on-chain query
872
- signers=signers,
946
+ signers=signers or [],
873
947
  )
874
948
 
875
- self.key_storage.accounts[service.safe_address] = safe_account
949
+ self.key_storage.accounts[address] = safe_account
876
950
  self.key_storage.save()
877
- logger.info(f"Imported Safe {service.safe_address} as '{tag}'")
951
+ logger.info(f"Imported Safe {address} as '{tag}'")
878
952
  return True, "ok"
879
953
 
880
954
  def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
@@ -901,7 +975,8 @@ class OlasServiceImporter:
901
975
  service_id=service.service_id,
902
976
  agent_ids=[25], # Trader agents always use agent ID 25
903
977
  multisig_address=service.safe_address,
904
- service_owner_address=service.service_owner_address,
978
+ service_owner_eoa_address=service.service_owner_eoa_address,
979
+ service_owner_multisig_address=service.service_owner_multisig_address,
905
980
  staking_contract_address=service.staking_contract_address,
906
981
  token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
907
982
  )
@@ -975,3 +1050,4 @@ class OlasServiceImporter:
975
1050
  key.signature_failed = True
976
1051
  logger.warning(f"Error verifying signature for key {key.address}: {e}")
977
1052
 
1053
+ FLAGS_OWNER_SAFE="deprecated"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, List, Optional
4
4
 
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel, Field, root_validator
6
6
 
7
7
  from iwa.core.models import EthereumAddress
8
8
 
@@ -14,12 +14,40 @@ class Service(BaseModel):
14
14
  chain_name: str
15
15
  service_id: int # Unique per chain
16
16
  agent_ids: List[int] = Field(default_factory=list) # List of agent type IDs
17
- service_owner_address: Optional[EthereumAddress] = None
17
+
18
+ # New explicit owner fields
19
+ service_owner_eoa_address: Optional[EthereumAddress] = None
20
+ service_owner_multisig_address: Optional[EthereumAddress] = None
21
+
22
+ # Deprecated fields (kept for migration, removed from physical model via aliasing/validation)
23
+ # Actually, we keep it optional but not used, or use migration logic.
24
+ # Let's remove it from fields and rely on before validator to map it to eoa.
25
+
18
26
  agent_address: Optional[EthereumAddress] = None
19
27
  multisig_address: Optional[EthereumAddress] = None
20
28
  staking_contract_address: Optional[EthereumAddress] = None
21
29
  token_address: Optional[EthereumAddress] = None
22
30
 
31
+ @root_validator(pre=True)
32
+ def migrate_owner_fields(cls, values): # noqa: N805
33
+ """Migrate legacy service_owner_address to service_owner_eoa_address."""
34
+ # Check for legacy 'service_owner_address'
35
+ if "service_owner_address" in values and values["service_owner_address"]:
36
+ legacy_addr = values["service_owner_address"]
37
+
38
+ # If service_owner_eoa_address is missing, use legacy
39
+ if "service_owner_eoa_address" not in values or not values["service_owner_eoa_address"]:
40
+ values["service_owner_eoa_address"] = legacy_addr
41
+
42
+ # Remove legacy field from values so it doesn't cause extra field errors if we removed it from model
43
+ # Or if strict.
44
+ return values
45
+
46
+ @property
47
+ def service_owner_address(self) -> Optional[EthereumAddress]:
48
+ """Backward compatibility property: Returns effective owner (Safe if present, else EOA)."""
49
+ return self.service_owner_multisig_address or self.service_owner_eoa_address
50
+
23
51
  @property
24
52
  def key(self) -> str:
25
53
  """Unique key for this service (chain_name:service_id)."""
@@ -106,5 +134,8 @@ class OlasConfig(BaseModel):
106
134
  target = multisig_address.lower()
107
135
  for service in self.services.values():
108
136
  if service.multisig_address and str(service.multisig_address).lower() == target:
137
+ # The following line is from the Code Edit, but it does not fit syntactically here.
138
+ # It appears to be from a different file (decoder.py) as indicated by the instruction.
139
+ # args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
109
140
  return service
110
141
  return None