iwa 0.0.20__tar.gz → 0.0.23__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.20/src/iwa.egg-info → iwa-0.0.23}/PKG-INFO +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/pyproject.toml +2 -2
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/interface.py +8 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/monitor.py +2 -2
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/safe.py +2 -2
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/safe.py +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/tests/test_safe.py +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/importer.py +261 -33
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/plugin.py +129 -43
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_plugin_full.py +7 -9
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/rpc.py +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/screens/wallets.py +2 -2
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/tests/test_rpc.py +2 -2
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/widgets/base.py +1 -1
- {iwa-0.0.20 → iwa-0.0.23/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_monitor.py +3 -3
- {iwa-0.0.20 → iwa-0.0.23}/LICENSE +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/README.md +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/setup.cfg +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/__main__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/chainlist.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/contract.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/db.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/test.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/types.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/server.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa.egg-info/SOURCES.txt +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_chain.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_cli.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_contract.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_db.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_keys.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_main.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_migration.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_modals.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_models.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_pricing.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_tables.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_utils.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tests/test_workers.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.20 → iwa-0.0.23}/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.23"
|
|
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.23"
|
|
75
75
|
fix = true
|
|
76
76
|
|
|
77
77
|
[tool.ruff.lint]
|
|
@@ -48,10 +48,17 @@ class ChainInterface:
|
|
|
48
48
|
self._rotation_lock = threading.Lock()
|
|
49
49
|
self._init_web3()
|
|
50
50
|
|
|
51
|
+
@property
|
|
52
|
+
def current_rpc(self) -> str:
|
|
53
|
+
"""Get the current active RPC URL."""
|
|
54
|
+
if not self.chain.rpcs:
|
|
55
|
+
return ""
|
|
56
|
+
return self.chain.rpcs[self._current_rpc_index]
|
|
57
|
+
|
|
51
58
|
@property
|
|
52
59
|
def is_tenderly(self) -> bool:
|
|
53
60
|
"""Check if connected to Tenderly vNet."""
|
|
54
|
-
rpc = self.
|
|
61
|
+
rpc = self.current_rpc or ""
|
|
55
62
|
return "tenderly" in rpc.lower() or "virtual" in rpc.lower()
|
|
56
63
|
|
|
57
64
|
def init_block_tracking(self):
|
|
@@ -24,7 +24,7 @@ class EventMonitor:
|
|
|
24
24
|
self.chain_interface = ChainInterfaces().get(chain_name)
|
|
25
25
|
self.web3 = self.chain_interface.web3
|
|
26
26
|
self.running = False
|
|
27
|
-
if self.chain_interface.
|
|
27
|
+
if self.chain_interface.current_rpc:
|
|
28
28
|
try:
|
|
29
29
|
self.last_checked_block = self.web3.eth.block_number
|
|
30
30
|
except Exception:
|
|
@@ -39,7 +39,7 @@ class EventMonitor:
|
|
|
39
39
|
f"Starting EventMonitor for {len(self.addresses)} addresses on {self.chain_interface.chain.name}"
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
if not self.chain_interface.
|
|
42
|
+
if not self.chain_interface.current_rpc:
|
|
43
43
|
logger.error(
|
|
44
44
|
f"Cannot start EventMonitor: No RPC URL found for chain {self.chain_interface.chain.name}"
|
|
45
45
|
)
|
|
@@ -102,7 +102,7 @@ class SafeService:
|
|
|
102
102
|
|
|
103
103
|
# Use ChainInterface which has proper RPC rotation and parsing
|
|
104
104
|
chain_interface = ChainInterfaces().get(chain_name)
|
|
105
|
-
return EthereumClient(chain_interface.
|
|
105
|
+
return EthereumClient(chain_interface.current_rpc)
|
|
106
106
|
|
|
107
107
|
def _deploy_safe_contract(
|
|
108
108
|
self,
|
|
@@ -254,7 +254,7 @@ class SafeService:
|
|
|
254
254
|
|
|
255
255
|
# Use ChainInterface which has proper RPC rotation and parsing
|
|
256
256
|
chain_interface = ChainInterfaces().get(chain)
|
|
257
|
-
ethereum_client = EthereumClient(chain_interface.
|
|
257
|
+
ethereum_client = EthereumClient(chain_interface.current_rpc)
|
|
258
258
|
|
|
259
259
|
code = ethereum_client.w3.eth.get_code(account.address)
|
|
260
260
|
|
|
@@ -30,7 +30,7 @@ class SafeMultisig:
|
|
|
30
30
|
from iwa.core.chain import ChainInterfaces
|
|
31
31
|
|
|
32
32
|
chain_interface = ChainInterfaces().get(chain_name.lower())
|
|
33
|
-
ethereum_client = EthereumClient(chain_interface.
|
|
33
|
+
ethereum_client = EthereumClient(chain_interface.current_rpc)
|
|
34
34
|
self.multisig = Safe(safe_account.address, ethereum_client)
|
|
35
35
|
self.ethereum_client = ethereum_client
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ def test_init(safe_account, mock_settings, mock_safe_eth):
|
|
|
42
42
|
"""Test initialization."""
|
|
43
43
|
with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
|
|
44
44
|
mock_ci = mock_ci_cls.return_value
|
|
45
|
-
mock_ci.get.return_value.
|
|
45
|
+
mock_ci.get.return_value.current_rpc = "http://rpc"
|
|
46
46
|
ms = SafeMultisig(safe_account, "gnosis")
|
|
47
47
|
assert ms.multisig is not None
|
|
48
48
|
mock_safe_eth[0].assert_called_with("http://rpc") # EthereumClient init
|
|
@@ -17,6 +17,28 @@ from loguru import logger
|
|
|
17
17
|
from iwa.core.keys import EncryptedAccount, KeyStorage
|
|
18
18
|
from iwa.core.models import Config, StoredSafeAccount
|
|
19
19
|
|
|
20
|
+
# Known mappings from olas-operate-middleware staking programs
|
|
21
|
+
# See: https://github.com/valory-xyz/olas-operate-middleware/blob/main/operate/ledger/profiles.py
|
|
22
|
+
STAKING_PROGRAM_MAP = {
|
|
23
|
+
# Pearl staking programs (gnosis) - operate format
|
|
24
|
+
"pearl_alpha": "0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e", # Expert Legacy
|
|
25
|
+
"pearl_beta": "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C", # Hobbyist 1 Legacy
|
|
26
|
+
"pearl_beta_2": "0xE56dF1E563De1B10715cB313D514af350D207212", # Expert 5 Legacy
|
|
27
|
+
"pearl_beta_3": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
|
|
28
|
+
"pearl_beta_4": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
|
|
29
|
+
"pearl_beta_5": "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f", # Expert 10 Legacy
|
|
30
|
+
"pearl_beta_mm_v2_1": "0x75eeca6207be98cac3fde8a20ecd7b01e50b3472", # Expert 3 MM v2
|
|
31
|
+
"pearl_beta_mm_v2_2": "0x9c7f6103e3a72e4d1805b9c683ea5b370ec1a99f", # Expert 4 MM v2
|
|
32
|
+
"pearl_beta_mm_v2_3": "0xcdC603e0Ee55Aae92519f9770f214b2Be4967f7d", # Expert 5 MM v2
|
|
33
|
+
# Quickstart staking programs (gnosis) - quickstart format
|
|
34
|
+
"quickstart_beta_expert_4": "0xaD9d891134443B443D7F30013c7e14Fe27F2E029", # Expert 4 Legacy
|
|
35
|
+
"quickstart_beta_expert_7": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
|
|
36
|
+
"quickstart_beta_expert_9": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
|
|
37
|
+
"quickstart_beta_expert_11": "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7", # Expert 11 Legacy
|
|
38
|
+
"quickstart_beta_expert_16_mech_marketplace": "0x6c65430515c70a3f5E62107CC301685B7D46f991", # Expert 16 MM v1
|
|
39
|
+
"quickstart_beta_expert_18_mech_marketplace": "0x041e679d04Fc0D4f75Eb937Dea729Df09a58e454", # Expert 18 MM v1
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
|
|
21
43
|
@dataclass
|
|
22
44
|
class DiscoveredKey:
|
|
@@ -26,8 +48,10 @@ class DiscoveredKey:
|
|
|
26
48
|
private_key: Optional[str] = None # Plaintext hex (None if still encrypted)
|
|
27
49
|
encrypted_keystore: Optional[dict] = None # Web3 v3 keystore format
|
|
28
50
|
source_file: Path = field(default_factory=Path)
|
|
29
|
-
role: str = "unknown" # "agent", "
|
|
51
|
+
role: str = "unknown" # "agent", "owner"
|
|
30
52
|
is_encrypted: bool = False
|
|
53
|
+
signature_verified: bool = False
|
|
54
|
+
signature_failed: bool = False
|
|
31
55
|
|
|
32
56
|
@property
|
|
33
57
|
def is_decrypted(self) -> bool:
|
|
@@ -55,6 +79,9 @@ class DiscoveredService:
|
|
|
55
79
|
source_folder: Path = field(default_factory=Path)
|
|
56
80
|
format: str = "unknown" # "trader_runner" or "operate"
|
|
57
81
|
service_name: Optional[str] = None
|
|
82
|
+
# New fields for full service import
|
|
83
|
+
staking_contract_address: Optional[str] = None
|
|
84
|
+
service_owner_address: Optional[str] = None
|
|
58
85
|
|
|
59
86
|
@property
|
|
60
87
|
def agent_key(self) -> Optional[DiscoveredKey]:
|
|
@@ -66,9 +93,14 @@ class DiscoveredService:
|
|
|
66
93
|
|
|
67
94
|
@property
|
|
68
95
|
def operator_key(self) -> Optional[DiscoveredKey]:
|
|
69
|
-
"""Get the operator key
|
|
96
|
+
"""Get the operator (owner) key. Alias for compatibility."""
|
|
97
|
+
return self.owner_key
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def owner_key(self) -> Optional[DiscoveredKey]:
|
|
101
|
+
"""Get the owner key if present (matches 'owner' or 'operator' roles)."""
|
|
70
102
|
for key in self.keys:
|
|
71
|
-
if key.role in
|
|
103
|
+
if key.role in ["owner", "operator"]:
|
|
72
104
|
return key
|
|
73
105
|
return None
|
|
74
106
|
|
|
@@ -89,15 +121,17 @@ class ImportResult:
|
|
|
89
121
|
class OlasServiceImporter:
|
|
90
122
|
"""Discover and import Olas services from external directories."""
|
|
91
123
|
|
|
92
|
-
def __init__(self, key_storage: Optional[KeyStorage] = None):
|
|
124
|
+
def __init__(self, key_storage: Optional[KeyStorage] = None, password: Optional[str] = None):
|
|
93
125
|
"""Initialize the importer.
|
|
94
126
|
|
|
95
127
|
Args:
|
|
96
128
|
key_storage: KeyStorage instance. If None, will create one.
|
|
129
|
+
password: Optional password to decrypt discovered keystores.
|
|
97
130
|
|
|
98
131
|
"""
|
|
99
132
|
self.key_storage = key_storage or KeyStorage()
|
|
100
133
|
self.config = Config()
|
|
134
|
+
self.password = password
|
|
101
135
|
|
|
102
136
|
def scan_directory(self, path: Path) -> List[DiscoveredService]:
|
|
103
137
|
"""Recursively scan a directory for Olas services.
|
|
@@ -106,7 +140,7 @@ class OlasServiceImporter:
|
|
|
106
140
|
path: Directory to scan.
|
|
107
141
|
|
|
108
142
|
Returns:
|
|
109
|
-
List of discovered services.
|
|
143
|
+
List of discovered services (deduplicated by chain:service_id).
|
|
110
144
|
|
|
111
145
|
"""
|
|
112
146
|
path = Path(path)
|
|
@@ -129,8 +163,51 @@ class OlasServiceImporter:
|
|
|
129
163
|
services = self._parse_operate_format(operate)
|
|
130
164
|
discovered.extend(services)
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
return self._deduplicate_services(discovered)
|
|
167
|
+
|
|
168
|
+
def _deduplicate_services(self, services: List[DiscoveredService]) -> List[DiscoveredService]:
|
|
169
|
+
"""Deduplicate discovered services by chain:service_id."""
|
|
170
|
+
seen_keys: set = set()
|
|
171
|
+
unique_services = []
|
|
172
|
+
duplicates = 0
|
|
173
|
+
for service in services:
|
|
174
|
+
if service.service_id:
|
|
175
|
+
key = f"{service.chain_name}:{service.service_id}"
|
|
176
|
+
if key in seen_keys:
|
|
177
|
+
logger.debug(
|
|
178
|
+
f"Skipping duplicate service {key} from {service.source_folder}"
|
|
179
|
+
)
|
|
180
|
+
duplicates += 1
|
|
181
|
+
continue
|
|
182
|
+
seen_keys.add(key)
|
|
183
|
+
unique_services.append(service)
|
|
184
|
+
|
|
185
|
+
if duplicates:
|
|
186
|
+
logger.info(f"Skipped {duplicates} duplicate service(s)")
|
|
187
|
+
logger.info(f"Discovered {len(unique_services)} unique Olas service(s)")
|
|
188
|
+
return unique_services
|
|
189
|
+
|
|
190
|
+
def _find_trader_name(self, folder: Path) -> str:
|
|
191
|
+
"""Find the trader name by traversing up the directory tree.
|
|
192
|
+
|
|
193
|
+
Handles quickstart format where the .operate folder is nested inside
|
|
194
|
+
a quickstart folder, e.g.: trader_altair/quickstart/.operate/
|
|
195
|
+
|
|
196
|
+
Returns the first folder name starting with 'trader_' or the
|
|
197
|
+
immediate folder name if none found.
|
|
198
|
+
"""
|
|
199
|
+
current = folder
|
|
200
|
+
fallback = folder.name
|
|
201
|
+
|
|
202
|
+
# Traverse up looking for trader_* folder
|
|
203
|
+
for _ in range(5): # Max 5 levels up
|
|
204
|
+
if current.name.startswith("trader_"):
|
|
205
|
+
return current.name
|
|
206
|
+
current = current.parent
|
|
207
|
+
if current == current.parent: # Reached root
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
return fallback
|
|
134
211
|
|
|
135
212
|
def _parse_trader_runner_format(self, folder: Path) -> Optional[DiscoveredService]:
|
|
136
213
|
"""Parse a .trader_runner folder.
|
|
@@ -153,6 +230,9 @@ class OlasServiceImporter:
|
|
|
153
230
|
service.safe_address = self._extract_safe_address(folder)
|
|
154
231
|
service.keys = self._extract_trader_keys(folder)
|
|
155
232
|
|
|
233
|
+
# Extract staking program from .env
|
|
234
|
+
self._extract_staking_from_env(service, folder)
|
|
235
|
+
|
|
156
236
|
if not service.keys and not service.service_id:
|
|
157
237
|
logger.debug(f"No valid data found in {folder}")
|
|
158
238
|
return None
|
|
@@ -187,12 +267,13 @@ class OlasServiceImporter:
|
|
|
187
267
|
if key:
|
|
188
268
|
keys.append(key)
|
|
189
269
|
|
|
190
|
-
# Parse operator_pkey.txt
|
|
270
|
+
# Parse operator_pkey.txt (contains owner key)
|
|
191
271
|
operator_file = folder / "operator_pkey.txt"
|
|
192
272
|
if operator_file.exists():
|
|
193
|
-
key = self._parse_keystore_file(operator_file, role="
|
|
273
|
+
key = self._parse_keystore_file(operator_file, role="owner")
|
|
194
274
|
if key:
|
|
195
275
|
keys.append(key)
|
|
276
|
+
self._verify_key_signature(key)
|
|
196
277
|
|
|
197
278
|
# Also check keys.json (array of keystores)
|
|
198
279
|
keys_file = folder / "keys.json"
|
|
@@ -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=
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -698,6 +866,7 @@ class OlasServiceImporter:
|
|
|
698
866
|
def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
|
|
699
867
|
"""Import service config to OlasConfig."""
|
|
700
868
|
try:
|
|
869
|
+
from iwa.plugins.olas.constants import OLAS_TOKEN_ADDRESS_GNOSIS
|
|
701
870
|
from iwa.plugins.olas.models import OlasConfig, Service
|
|
702
871
|
|
|
703
872
|
# Get or create OlasConfig
|
|
@@ -711,13 +880,16 @@ class OlasServiceImporter:
|
|
|
711
880
|
if key in olas_config.services:
|
|
712
881
|
return False, "duplicate"
|
|
713
882
|
|
|
714
|
-
# Create service model
|
|
883
|
+
# Create service model with all fields
|
|
715
884
|
olas_service = Service(
|
|
716
885
|
service_name=service.service_name or f"service_{service.service_id}",
|
|
717
886
|
chain_name=service.chain_name,
|
|
718
887
|
service_id=service.service_id,
|
|
719
|
-
agent_ids=[], #
|
|
888
|
+
agent_ids=[25], # Trader agents always use agent ID 25
|
|
720
889
|
multisig_address=service.safe_address,
|
|
890
|
+
service_owner_address=service.service_owner_address,
|
|
891
|
+
staking_contract_address=service.staking_contract_address,
|
|
892
|
+
token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
|
|
721
893
|
)
|
|
722
894
|
|
|
723
895
|
# Set agent address if we have one
|
|
@@ -734,3 +906,59 @@ class OlasServiceImporter:
|
|
|
734
906
|
return False, "Olas plugin not available"
|
|
735
907
|
except Exception as e:
|
|
736
908
|
return False, str(e)
|
|
909
|
+
|
|
910
|
+
def _attempt_decryption(self, key: DiscoveredKey) -> None:
|
|
911
|
+
"""Attempt to decrypt an encrypted keystore using the provided password."""
|
|
912
|
+
if not self.password or not key.encrypted_keystore:
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
try:
|
|
916
|
+
logger.debug(f"Attempting decryption for {key.address}")
|
|
917
|
+
|
|
918
|
+
# Use Account.decrypt to handle standard web3 keystores
|
|
919
|
+
private_key_bytes = Account.decrypt(key.encrypted_keystore, self.password)
|
|
920
|
+
key.private_key = private_key_bytes.hex()
|
|
921
|
+
key.is_encrypted = False
|
|
922
|
+
# If we successfully decrypted, it's no longer "encrypted" for verification purposes
|
|
923
|
+
logger.debug(f"Successfully decrypted key for {key.address}")
|
|
924
|
+
except ValueError as e:
|
|
925
|
+
# Password incorrect
|
|
926
|
+
logger.warning(f"Decryption failed (ValueError) for {key.address}: {e}")
|
|
927
|
+
pass
|
|
928
|
+
except Exception as e:
|
|
929
|
+
logger.warning(f"Error decrypting key {key.address}: {type(e).__name__} - {e}")
|
|
930
|
+
|
|
931
|
+
def _verify_key_signature(self, key: DiscoveredKey) -> None:
|
|
932
|
+
"""Verify that the plaintext private key can sign a message and recover the address."""
|
|
933
|
+
if not key.private_key or not key.address:
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
try:
|
|
937
|
+
from eth_account.messages import encode_defunct
|
|
938
|
+
|
|
939
|
+
message = "Hello, world!"
|
|
940
|
+
encoded_message = encode_defunct(text=message)
|
|
941
|
+
signed_message = Account.sign_message(encoded_message, private_key=key.private_key)
|
|
942
|
+
recovered_address = Account.recover_message(
|
|
943
|
+
encoded_message, signature=signed_message.signature
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
# Normalize address to lowercase with 0x prefix
|
|
947
|
+
key_addr = key.address.lower()
|
|
948
|
+
if not key_addr.startswith("0x"):
|
|
949
|
+
key_addr = "0x" + key_addr
|
|
950
|
+
recovered_addr = recovered_address.lower()
|
|
951
|
+
|
|
952
|
+
if recovered_addr == key_addr:
|
|
953
|
+
key.signature_verified = True
|
|
954
|
+
logger.debug(f"Signature verified for key {key.address}")
|
|
955
|
+
else:
|
|
956
|
+
key.signature_failed = True
|
|
957
|
+
logger.warning(
|
|
958
|
+
f"Signature verification FAILED for key {key.address}. "
|
|
959
|
+
f"Recovered: {recovered_address}"
|
|
960
|
+
)
|
|
961
|
+
except Exception as e:
|
|
962
|
+
key.signature_failed = True
|
|
963
|
+
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
964
|
+
|