iwa 0.0.27__tar.gz → 0.0.29__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.
- {iwa-0.0.27/src/iwa.egg-info → iwa-0.0.29}/PKG-INFO +1 -1
- {iwa-0.0.27 → iwa-0.0.29}/pyproject.toml +2 -2
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/cli.py +18 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/contract.py +12 -1
- iwa-0.0.29/src/iwa/core/contracts/decoder.py +154 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transaction.py +63 -2
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/wallet.py +1 -1
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/importer.py +111 -29
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/models.py +33 -2
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/plugin.py +68 -17
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +5 -17
- {iwa-0.0.27 → iwa-0.0.29/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa.egg-info/SOURCES.txt +1 -0
- {iwa-0.0.27 → iwa-0.0.29}/LICENSE +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/README.md +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/setup.cfg +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/__main__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/interface.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/chainlist.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/db.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/models.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/safe.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/test.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/types.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/models.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/server.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_chain.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_cli.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_contract.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_db.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_keys.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_main.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_migration.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_modals.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_models.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_pricing.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_tables.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_utils.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tests/test_workers.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.27 → iwa-0.0.29}/src/tools/verify_drain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "iwa"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.29"
|
|
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.
|
|
74
|
+
target-version = "0.0.29"
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
545
|
-
if service.
|
|
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.
|
|
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(
|
|
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,44 @@ 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
|
+
addr = key.address
|
|
801
|
+
if not addr.startswith("0x"):
|
|
802
|
+
addr = "0x" + addr
|
|
803
|
+
signers.append(addr)
|
|
804
|
+
return signers
|
|
805
|
+
|
|
806
|
+
def _get_owner_signers(self, service: DiscoveredService) -> List[str]:
|
|
807
|
+
"""Get list of signers for the owner safe."""
|
|
808
|
+
signers = []
|
|
809
|
+
for key in service.keys:
|
|
810
|
+
# We look for keys marked as owner/operator
|
|
811
|
+
if key.role in ["owner", "operator"]:
|
|
812
|
+
addr = key.address
|
|
813
|
+
if not addr.startswith("0x"):
|
|
814
|
+
addr = "0x" + addr
|
|
815
|
+
signers.append(addr)
|
|
816
|
+
return signers
|
|
817
|
+
|
|
737
818
|
def _import_discovered_service_config(
|
|
738
819
|
self, service: DiscoveredService, result: ImportResult
|
|
739
820
|
) -> None:
|
|
@@ -833,27 +914,26 @@ class OlasServiceImporter:
|
|
|
833
914
|
i += 1
|
|
834
915
|
return f"{base_tag}_{i}"
|
|
835
916
|
|
|
836
|
-
def _import_safe(
|
|
837
|
-
|
|
838
|
-
|
|
917
|
+
def _import_safe(
|
|
918
|
+
self,
|
|
919
|
+
address: str,
|
|
920
|
+
signers: List[str] = None,
|
|
921
|
+
tag_suffix: str = "safe",
|
|
922
|
+
service_name: Optional[str] = None
|
|
923
|
+
) -> Tuple[bool, str]:
|
|
924
|
+
"""Import a generic Safe."""
|
|
925
|
+
if not address:
|
|
839
926
|
return False, "no safe address"
|
|
840
927
|
|
|
841
928
|
# Check for duplicate
|
|
842
|
-
existing = self.key_storage.find_stored_account(
|
|
929
|
+
existing = self.key_storage.find_stored_account(address)
|
|
843
930
|
if existing:
|
|
844
931
|
return False, "duplicate"
|
|
845
932
|
|
|
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
933
|
# Generate tag
|
|
853
|
-
|
|
854
|
-
prefix = service.service_name or "imported"
|
|
934
|
+
prefix = service_name or "imported"
|
|
855
935
|
prefix = re.sub(r"[^a-z0-9]+", "_", prefix.lower()).strip("_")
|
|
856
|
-
base_tag = f"{prefix}
|
|
936
|
+
base_tag = f"{prefix}_{tag_suffix}"
|
|
857
937
|
|
|
858
938
|
existing_tags = {
|
|
859
939
|
acc.tag for acc in self.key_storage.accounts.values() if hasattr(acc, "tag")
|
|
@@ -866,15 +946,15 @@ class OlasServiceImporter:
|
|
|
866
946
|
|
|
867
947
|
safe_account = StoredSafeAccount(
|
|
868
948
|
tag=tag,
|
|
869
|
-
address=
|
|
870
|
-
chains=[
|
|
949
|
+
address=address,
|
|
950
|
+
chains=["gnosis"], # TODO: detecting chain dynamically would be better
|
|
871
951
|
threshold=1, # Default, accurate value requires on-chain query
|
|
872
|
-
signers=signers,
|
|
952
|
+
signers=signers or [],
|
|
873
953
|
)
|
|
874
954
|
|
|
875
|
-
self.key_storage.accounts[
|
|
955
|
+
self.key_storage.accounts[address] = safe_account
|
|
876
956
|
self.key_storage.save()
|
|
877
|
-
logger.info(f"Imported Safe {
|
|
957
|
+
logger.info(f"Imported Safe {address} as '{tag}'")
|
|
878
958
|
return True, "ok"
|
|
879
959
|
|
|
880
960
|
def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
|
|
@@ -901,7 +981,8 @@ class OlasServiceImporter:
|
|
|
901
981
|
service_id=service.service_id,
|
|
902
982
|
agent_ids=[25], # Trader agents always use agent ID 25
|
|
903
983
|
multisig_address=service.safe_address,
|
|
904
|
-
|
|
984
|
+
service_owner_eoa_address=service.service_owner_eoa_address,
|
|
985
|
+
service_owner_multisig_address=service.service_owner_multisig_address,
|
|
905
986
|
staking_contract_address=service.staking_contract_address,
|
|
906
987
|
token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
|
|
907
988
|
)
|
|
@@ -975,3 +1056,4 @@ class OlasServiceImporter:
|
|
|
975
1056
|
key.signature_failed = True
|
|
976
1057
|
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
977
1058
|
|
|
1059
|
+
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
|
-
|
|
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
|