iwa 0.0.61__tar.gz → 0.0.64__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.61/src/iwa.egg-info → iwa-0.0.64}/PKG-INFO +1 -1
- {iwa-0.0.61 → iwa-0.0.64}/pyproject.toml +2 -2
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/interface.py +26 -12
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/safe_executor.py +110 -26
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/models.py +5 -22
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/drain.py +34 -12
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/lifecycle.py +3 -3
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_models.py +5 -5
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +1 -1
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +6 -1
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_staking.py +1 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.61 → iwa-0.0.64/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa.egg-info/SOURCES.txt +1 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_chainlist_enrichment.py +235 -2
- iwa-0.0.64/src/tests/test_contract_cache.py +253 -0
- iwa-0.0.64/src/tests/test_drain_coverage.py +442 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_safe_executor.py +70 -0
- iwa-0.0.64/src/tests/test_staking_simple.py +503 -0
- iwa-0.0.61/src/tests/test_drain_coverage.py +0 -180
- iwa-0.0.61/src/tests/test_staking_simple.py +0 -31
- {iwa-0.0.61 → iwa-0.0.64}/LICENSE +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/README.md +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/setup.cfg +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/__main__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/chainlist.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/contract.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/decoder.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/db.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/http.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/models.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/safe.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/test.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tests/test_ipfs.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tests/test_pricing.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tests/test_regression_fixes.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/types.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/importer.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/plugin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/drain_accounts.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/models.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/server.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_chain.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_cli.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_contract.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_db.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_keys.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_main.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_migration.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_modals.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_models.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rate_limiter_retry.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rpc_rate_limit.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_safe_integration.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_tables.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_utils.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tests/test_workers.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.61 → iwa-0.0.64}/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.64"
|
|
4
4
|
description = "A secure, modular, and plugin-based framework for crypto agents and ops"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<4.0"
|
|
@@ -72,7 +72,7 @@ where = ["src"]
|
|
|
72
72
|
|
|
73
73
|
[tool.ruff]
|
|
74
74
|
line-length = 100
|
|
75
|
-
target-version = "0.0.
|
|
75
|
+
target-version = "0.0.64"
|
|
76
76
|
fix = true
|
|
77
77
|
|
|
78
78
|
[tool.ruff.lint]
|
|
@@ -285,11 +285,15 @@ class ChainInterface:
|
|
|
285
285
|
|
|
286
286
|
# -- ChainList enrichment ----------------------------------------------
|
|
287
287
|
|
|
288
|
-
MAX_RPCS =
|
|
288
|
+
MAX_RPCS = 20 # Cap total RPCs per chain
|
|
289
289
|
|
|
290
290
|
def _enrich_rpcs_from_chainlist(self) -> None:
|
|
291
291
|
"""Add validated public RPCs from ChainList to the rotation pool."""
|
|
292
292
|
if len(self.chain.rpcs) >= self.MAX_RPCS:
|
|
293
|
+
logger.debug(
|
|
294
|
+
f"{self.chain.name}: skipping ChainList enrichment "
|
|
295
|
+
f"(already have {len(self.chain.rpcs)} RPCs)"
|
|
296
|
+
)
|
|
293
297
|
return
|
|
294
298
|
|
|
295
299
|
try:
|
|
@@ -354,35 +358,45 @@ class ChainInterface:
|
|
|
354
358
|
|
|
355
359
|
if should_rotate:
|
|
356
360
|
failed_index = self._current_rpc_index
|
|
361
|
+
failed_rpc = sanitize_rpc_url(self.chain.rpcs[failed_index]) if self.chain.rpcs else "?"
|
|
357
362
|
|
|
358
363
|
# Apply per-RPC backoff so smart rotation skips this RPC.
|
|
359
364
|
if result["is_quota_exceeded"]:
|
|
360
|
-
error_type = "
|
|
361
|
-
|
|
365
|
+
error_type = "QUOTA"
|
|
366
|
+
backoff = self.QUOTA_EXCEEDED_BACKOFF
|
|
367
|
+
self._mark_rpc_backoff(failed_index, backoff)
|
|
362
368
|
elif result["is_rate_limit"]:
|
|
363
|
-
error_type = "
|
|
364
|
-
|
|
369
|
+
error_type = "RATE_LIMIT"
|
|
370
|
+
backoff = self.RATE_LIMIT_BACKOFF
|
|
371
|
+
self._mark_rpc_backoff(failed_index, backoff)
|
|
365
372
|
# Brief global backoff so other threads don't immediately flood
|
|
366
373
|
# the same (now backed-off) RPC before rotation takes effect.
|
|
367
374
|
self._rate_limiter.trigger_backoff(seconds=2.0)
|
|
368
375
|
else:
|
|
369
|
-
error_type = "
|
|
370
|
-
|
|
376
|
+
error_type = "CONNECTION"
|
|
377
|
+
backoff = self.CONNECTION_ERROR_BACKOFF
|
|
378
|
+
self._mark_rpc_backoff(failed_index, backoff)
|
|
379
|
+
|
|
380
|
+
# Count healthy RPCs for visibility
|
|
381
|
+
healthy_count = sum(1 for i in range(len(self.chain.rpcs)) if self._is_rpc_healthy(i))
|
|
382
|
+
total_rpcs = len(self.chain.rpcs) if self.chain.rpcs else 0
|
|
371
383
|
|
|
372
384
|
logger.warning(
|
|
373
|
-
f"
|
|
374
|
-
f"(
|
|
385
|
+
f"[{self.chain.name}] RPC #{failed_index} {error_type} → "
|
|
386
|
+
f"backoff {int(backoff)}s ({healthy_count}/{total_rpcs} healthy) | "
|
|
387
|
+
f"{failed_rpc}: {str(error)[:100]}"
|
|
375
388
|
)
|
|
376
389
|
|
|
377
390
|
if self.rotate_rpc():
|
|
378
391
|
result["rotated"] = True
|
|
379
392
|
result["should_retry"] = True
|
|
380
|
-
|
|
393
|
+
new_rpc = sanitize_rpc_url(self.chain.rpcs[self._current_rpc_index])
|
|
394
|
+
logger.info(f"[{self.chain.name}] Rotated to RPC #{self._current_rpc_index}: {new_rpc}")
|
|
381
395
|
else:
|
|
382
396
|
# Rotation skipped (cooldown or single RPC) - still allow retry
|
|
383
397
|
result["should_retry"] = True
|
|
384
|
-
logger.
|
|
385
|
-
f"
|
|
398
|
+
logger.debug(
|
|
399
|
+
f"[{self.chain.name}] Rotation skipped (cooldown), retrying RPC #{self._current_rpc_index}"
|
|
386
400
|
)
|
|
387
401
|
|
|
388
402
|
elif result["is_server_error"]:
|
|
@@ -39,6 +39,10 @@ class SafeTransactionExecutor:
|
|
|
39
39
|
MAX_GAS_MULTIPLIER = 10 # Hard cap: never exceed 10x original estimate
|
|
40
40
|
DEFAULT_FALLBACK_GAS = 500_000 # Fallback when estimation fails
|
|
41
41
|
|
|
42
|
+
# Fee bumping for "max fee per gas less than block base fee" errors
|
|
43
|
+
FEE_BUMP_PERCENTAGE = 1.30 # 30% bump per retry on fee errors
|
|
44
|
+
MAX_FEE_BUMP_FACTOR = 3.0 # Cap: never bump more than 3x original
|
|
45
|
+
|
|
42
46
|
def __init__(
|
|
43
47
|
self,
|
|
44
48
|
chain_interface: "ChainInterface",
|
|
@@ -76,6 +80,7 @@ class SafeTransactionExecutor:
|
|
|
76
80
|
last_error = None
|
|
77
81
|
current_gas = safe_tx.safe_tx_gas
|
|
78
82
|
base_estimate = current_gas if current_gas > 0 else 0
|
|
83
|
+
fee_bump_factor = 1.0 # Multiplier for EIP-1559 fees, increases on fee errors
|
|
79
84
|
|
|
80
85
|
for attempt in range(self.max_retries + 1):
|
|
81
86
|
SAFE_TX_STATS["total_attempts"] += 1
|
|
@@ -89,6 +94,7 @@ class SafeTransactionExecutor:
|
|
|
89
94
|
attempt,
|
|
90
95
|
current_gas,
|
|
91
96
|
base_estimate,
|
|
97
|
+
fee_bump_factor,
|
|
92
98
|
)
|
|
93
99
|
|
|
94
100
|
# Check receipt
|
|
@@ -106,7 +112,7 @@ class SafeTransactionExecutor:
|
|
|
106
112
|
raise ValueError("Transaction reverted on-chain")
|
|
107
113
|
|
|
108
114
|
except Exception as e:
|
|
109
|
-
updated_tx, should_retry = self._handle_execution_failure(
|
|
115
|
+
updated_tx, should_retry, is_fee_error = self._handle_execution_failure(
|
|
110
116
|
e, safe_address, safe_tx, attempt, operation_name
|
|
111
117
|
)
|
|
112
118
|
last_error = e
|
|
@@ -115,7 +121,12 @@ class SafeTransactionExecutor:
|
|
|
115
121
|
|
|
116
122
|
# Update gas/nonce for next loop if needed
|
|
117
123
|
safe_tx = updated_tx
|
|
118
|
-
|
|
124
|
+
|
|
125
|
+
# Bump fee multiplier on fee-related errors (base fee > max fee)
|
|
126
|
+
if is_fee_error and fee_bump_factor < self.MAX_FEE_BUMP_FACTOR:
|
|
127
|
+
fee_bump_factor *= self.FEE_BUMP_PERCENTAGE
|
|
128
|
+
fee_bump_factor = min(fee_bump_factor, self.MAX_FEE_BUMP_FACTOR)
|
|
129
|
+
logger.info(f"[{operation_name}] Fee bump factor increased to {fee_bump_factor:.2f}x")
|
|
119
130
|
|
|
120
131
|
delay = self.DEFAULT_RETRY_DELAY * (2**attempt)
|
|
121
132
|
time.sleep(delay)
|
|
@@ -131,6 +142,7 @@ class SafeTransactionExecutor:
|
|
|
131
142
|
attempt,
|
|
132
143
|
current_gas,
|
|
133
144
|
base_estimate,
|
|
145
|
+
fee_bump_factor: float = 1.0,
|
|
134
146
|
) -> str:
|
|
135
147
|
"""Prepare client, estimate gas, simulate, and execute."""
|
|
136
148
|
# 1. (Re)Create Safe client
|
|
@@ -171,24 +183,11 @@ class SafeTransactionExecutor:
|
|
|
171
183
|
signatures_backup = safe_tx.signatures
|
|
172
184
|
|
|
173
185
|
try:
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# Handle both tuple return (tx_hash, tx) and bytes return
|
|
180
|
-
if isinstance(result, tuple):
|
|
181
|
-
tx_hash_bytes = result[0]
|
|
182
|
-
else:
|
|
183
|
-
tx_hash_bytes = result
|
|
184
|
-
|
|
185
|
-
# Handle both bytes and hex string returns
|
|
186
|
-
if isinstance(tx_hash_bytes, bytes):
|
|
187
|
-
return f"0x{tx_hash_bytes.hex()}"
|
|
188
|
-
elif isinstance(tx_hash_bytes, str):
|
|
189
|
-
return tx_hash_bytes if tx_hash_bytes.startswith("0x") else f"0x{tx_hash_bytes}"
|
|
190
|
-
else:
|
|
191
|
-
return str(tx_hash_bytes)
|
|
186
|
+
# Execute with appropriate gas pricing
|
|
187
|
+
result = self._execute_with_gas_pricing(
|
|
188
|
+
safe_tx, signer_keys[0], fee_bump_factor, operation_name
|
|
189
|
+
)
|
|
190
|
+
return self._extract_tx_hash(result)
|
|
192
191
|
|
|
193
192
|
finally:
|
|
194
193
|
# Restore signatures for next attempt if needed
|
|
@@ -196,6 +195,39 @@ class SafeTransactionExecutor:
|
|
|
196
195
|
if safe_tx.signatures != signatures_backup:
|
|
197
196
|
safe_tx.signatures = signatures_backup
|
|
198
197
|
|
|
198
|
+
def _execute_with_gas_pricing(
|
|
199
|
+
self, safe_tx: SafeTx, signer_key: str, fee_bump_factor: float, operation_name: str
|
|
200
|
+
):
|
|
201
|
+
"""Execute transaction with appropriate gas pricing strategy.
|
|
202
|
+
|
|
203
|
+
If fee_bump_factor > 1.0, calculates a bumped gas price to overcome
|
|
204
|
+
base fee volatility. Otherwise uses EIP-1559 FAST speed.
|
|
205
|
+
"""
|
|
206
|
+
if fee_bump_factor > 1.0:
|
|
207
|
+
bumped_gas_price = self._calculate_bumped_gas_price(fee_bump_factor)
|
|
208
|
+
if bumped_gas_price:
|
|
209
|
+
logger.debug(
|
|
210
|
+
f"[{operation_name}] Using bumped gas price: {bumped_gas_price} wei "
|
|
211
|
+
f"(factor: {fee_bump_factor:.2f}x)"
|
|
212
|
+
)
|
|
213
|
+
return safe_tx.execute(signer_key, tx_gas_price=bumped_gas_price)
|
|
214
|
+
# Fallback to FAST if calculation fails
|
|
215
|
+
return safe_tx.execute(signer_key, eip1559_speed=TxSpeed.FAST)
|
|
216
|
+
# Default: use EIP-1559 'FAST' speed
|
|
217
|
+
return safe_tx.execute(signer_key, eip1559_speed=TxSpeed.FAST)
|
|
218
|
+
|
|
219
|
+
def _extract_tx_hash(self, result) -> str:
|
|
220
|
+
"""Extract transaction hash from execute() result."""
|
|
221
|
+
# Handle both tuple return (tx_hash, tx) and bytes return
|
|
222
|
+
tx_hash_bytes = result[0] if isinstance(result, tuple) else result
|
|
223
|
+
|
|
224
|
+
# Handle both bytes and hex string returns
|
|
225
|
+
if isinstance(tx_hash_bytes, bytes):
|
|
226
|
+
return f"0x{tx_hash_bytes.hex()}"
|
|
227
|
+
if isinstance(tx_hash_bytes, str):
|
|
228
|
+
return tx_hash_bytes if tx_hash_bytes.startswith("0x") else f"0x{tx_hash_bytes}"
|
|
229
|
+
return str(tx_hash_bytes)
|
|
230
|
+
|
|
199
231
|
def _check_receipt_status(self, receipt) -> bool:
|
|
200
232
|
"""Check if receipt has successful status."""
|
|
201
233
|
status = getattr(receipt, "status", None)
|
|
@@ -210,14 +242,20 @@ class SafeTransactionExecutor:
|
|
|
210
242
|
safe_tx: SafeTx,
|
|
211
243
|
attempt: int,
|
|
212
244
|
operation_name: str,
|
|
213
|
-
) -> Tuple[SafeTx, bool]:
|
|
214
|
-
"""Handle execution failure and determine next steps.
|
|
245
|
+
) -> Tuple[SafeTx, bool, bool]:
|
|
246
|
+
"""Handle execution failure and determine next steps.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (updated_safe_tx, should_retry, is_fee_error)
|
|
250
|
+
|
|
251
|
+
"""
|
|
215
252
|
classification = self._classify_error(error)
|
|
253
|
+
is_fee_error = classification["is_fee_error"]
|
|
216
254
|
|
|
217
255
|
if attempt >= self.max_retries:
|
|
218
256
|
SAFE_TX_STATS["final_failures"] += 1
|
|
219
257
|
logger.error(f"[{operation_name}] Failed after {attempt + 1} attempts: {error}")
|
|
220
|
-
return safe_tx, False
|
|
258
|
+
return safe_tx, False, is_fee_error
|
|
221
259
|
|
|
222
260
|
strategy = "retry"
|
|
223
261
|
safe = self._recreate_safe_client(safe_address)
|
|
@@ -231,13 +269,16 @@ class SafeTransactionExecutor:
|
|
|
231
269
|
SAFE_TX_STATS["rpc_rotations"] += 1
|
|
232
270
|
result = self.chain_interface._handle_rpc_error(error)
|
|
233
271
|
if not result["should_retry"]:
|
|
234
|
-
return safe_tx, False
|
|
272
|
+
return safe_tx, False, is_fee_error
|
|
273
|
+
elif is_fee_error:
|
|
274
|
+
strategy = "fee bump"
|
|
275
|
+
SAFE_TX_STATS["gas_retries"] += 1
|
|
235
276
|
elif classification["is_gas_error"]:
|
|
236
277
|
strategy = "gas increase"
|
|
237
|
-
|
|
278
|
+
SAFE_TX_STATS["gas_retries"] += 1
|
|
238
279
|
|
|
239
280
|
self._log_retry(attempt + 1, error, strategy)
|
|
240
|
-
return safe_tx, True
|
|
281
|
+
return safe_tx, True, is_fee_error
|
|
241
282
|
|
|
242
283
|
def _estimate_safe_tx_gas(self, safe: Safe, safe_tx: SafeTx, base_estimate: int = 0) -> int:
|
|
243
284
|
"""Estimate gas for a Safe transaction with buffer and hard cap."""
|
|
@@ -320,14 +361,57 @@ class SafeTransactionExecutor:
|
|
|
320
361
|
error
|
|
321
362
|
) or self.chain_interface._is_connection_error(error)
|
|
322
363
|
|
|
364
|
+
# Fee-specific errors: base fee jumped above our max fee
|
|
365
|
+
fee_error_signals = [
|
|
366
|
+
"max fee per gas less than block base fee",
|
|
367
|
+
"maxfeepergas",
|
|
368
|
+
"fee too low",
|
|
369
|
+
"underpriced",
|
|
370
|
+
]
|
|
371
|
+
is_fee_error = any(signal in err_text for signal in fee_error_signals)
|
|
372
|
+
|
|
323
373
|
return {
|
|
324
374
|
"is_gas_error": any(x in err_text for x in ["gas", "out of gas", "intrinsic"]),
|
|
375
|
+
"is_fee_error": is_fee_error,
|
|
325
376
|
"is_nonce_error": self._is_nonce_error(error),
|
|
326
377
|
"is_rpc_error": is_rpc,
|
|
327
378
|
"is_revert": "revert" in err_text or "execution reverted" in err_text,
|
|
328
379
|
"is_signature_error": self._is_signature_error(error),
|
|
329
380
|
}
|
|
330
381
|
|
|
382
|
+
def _calculate_bumped_gas_price(self, bump_factor: float) -> Optional[int]:
|
|
383
|
+
"""Calculate a bumped gas price based on current base fee.
|
|
384
|
+
|
|
385
|
+
Uses legacy gas price (not EIP-1559) for compatibility with safe-eth-py's
|
|
386
|
+
tx_gas_price parameter. The bumped price ensures we're above the current
|
|
387
|
+
base fee even if it's volatile.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
bump_factor: Multiplier to apply to the base fee (e.g., 1.3 = 30% bump)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Gas price in wei, or None if calculation fails
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
web3 = self.chain_interface.web3
|
|
398
|
+
latest_block = web3.eth.get_block("latest")
|
|
399
|
+
base_fee = latest_block.get("baseFeePerGas")
|
|
400
|
+
|
|
401
|
+
if base_fee is not None:
|
|
402
|
+
# EIP-1559 chain: calculate bumped max fee
|
|
403
|
+
# base_fee * bump_factor * 1.5 (extra buffer) + priority fee
|
|
404
|
+
priority_fee = max(int(web3.eth.max_priority_fee), 1)
|
|
405
|
+
bumped_fee = int(base_fee * bump_factor * 1.5) + priority_fee
|
|
406
|
+
return bumped_fee
|
|
407
|
+
else:
|
|
408
|
+
# Legacy chain: bump the gas price directly
|
|
409
|
+
gas_price = web3.eth.gas_price
|
|
410
|
+
return int(gas_price * bump_factor)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.debug(f"Failed to calculate bumped gas price: {e}")
|
|
413
|
+
return None
|
|
414
|
+
|
|
331
415
|
def _decode_revert_reason(self, error: Exception) -> Optional[str]:
|
|
332
416
|
"""Attempt to decode the revert reason."""
|
|
333
417
|
import re
|
|
@@ -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
|
|
6
6
|
|
|
7
7
|
from iwa.core.models import EthereumAddress
|
|
8
8
|
|
|
@@ -15,37 +15,20 @@ class Service(BaseModel):
|
|
|
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
|
-
#
|
|
18
|
+
# Owner fields:
|
|
19
|
+
# - service_owner_eoa_address: EOA that owns the service (or signs for the multisig)
|
|
20
|
+
# - service_owner_multisig_address: Safe multisig owner (optional, if service is owned by a Safe)
|
|
19
21
|
service_owner_eoa_address: Optional[EthereumAddress] = None
|
|
20
22
|
service_owner_multisig_address: Optional[EthereumAddress] = None
|
|
21
23
|
|
|
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
|
-
|
|
26
24
|
agent_address: Optional[EthereumAddress] = None
|
|
27
25
|
multisig_address: Optional[EthereumAddress] = None
|
|
28
26
|
staking_contract_address: Optional[EthereumAddress] = None
|
|
29
27
|
token_address: Optional[EthereumAddress] = None
|
|
30
28
|
|
|
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
29
|
@property
|
|
47
30
|
def service_owner_address(self) -> Optional[EthereumAddress]:
|
|
48
|
-
"""
|
|
31
|
+
"""Returns effective owner address (Safe multisig if present, else EOA)."""
|
|
49
32
|
return self.service_owner_multisig_address or self.service_owner_eoa_address
|
|
50
33
|
|
|
51
34
|
@property
|
|
@@ -12,7 +12,9 @@ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
|
12
12
|
class DrainManagerMixin:
|
|
13
13
|
"""Mixin for draining and service token management."""
|
|
14
14
|
|
|
15
|
-
def claim_rewards(
|
|
15
|
+
def claim_rewards( # noqa: C901
|
|
16
|
+
self, staking_contract: Optional[StakingContract] = None
|
|
17
|
+
) -> Tuple[bool, int]:
|
|
16
18
|
"""Claim staking rewards for the active service.
|
|
17
19
|
|
|
18
20
|
The claimed OLAS tokens will be sent to the service's multisig (Safe).
|
|
@@ -51,13 +53,19 @@ class DrainManagerMixin:
|
|
|
51
53
|
logger.info("Service not staked, skipping claim")
|
|
52
54
|
return False, 0
|
|
53
55
|
|
|
54
|
-
# Check
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
# Check claimable rewards using calculate_staking_reward for accurate value
|
|
57
|
+
# (get_accrued_rewards returns stored value which may be outdated)
|
|
58
|
+
try:
|
|
59
|
+
claimable_rewards = staking_contract.calculate_staking_reward(service_id)
|
|
60
|
+
except Exception:
|
|
61
|
+
# Fallback to stored value if calculation fails
|
|
62
|
+
claimable_rewards = staking_contract.get_accrued_rewards(service_id)
|
|
63
|
+
|
|
64
|
+
if claimable_rewards == 0:
|
|
65
|
+
logger.info("No rewards to claim")
|
|
58
66
|
return False, 0
|
|
59
67
|
|
|
60
|
-
logger.info(f"Claiming {
|
|
68
|
+
logger.info(f"Claiming ~{claimable_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
|
|
61
69
|
|
|
62
70
|
# Use service owner which holds the reward rights (not necessarily master)
|
|
63
71
|
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
@@ -83,11 +91,19 @@ class DrainManagerMixin:
|
|
|
83
91
|
return False, 0
|
|
84
92
|
|
|
85
93
|
events = staking_contract.extract_events(receipt)
|
|
86
|
-
if "RewardClaimed" not in [event["name"] for event in events]:
|
|
87
|
-
logger.warning("RewardClaimed event not found, but transaction succeeded")
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
# Extract actual claimed amount from RewardClaimed event
|
|
96
|
+
claimed_amount = claimable_rewards # Default to estimated
|
|
97
|
+
for event in events:
|
|
98
|
+
if event["name"] == "RewardClaimed":
|
|
99
|
+
# RewardClaimed event has 'amount' or 'reward' field
|
|
100
|
+
claimed_amount = event["args"].get("amount", event["args"].get("reward", claimed_amount))
|
|
101
|
+
break
|
|
102
|
+
else:
|
|
103
|
+
logger.warning("RewardClaimed event not found, using estimated amount")
|
|
104
|
+
|
|
105
|
+
logger.info(f"Successfully claimed {claimed_amount / 1e18:.4f} OLAS rewards")
|
|
106
|
+
return True, claimed_amount
|
|
91
107
|
|
|
92
108
|
def withdraw_rewards(self) -> Tuple[bool, float]:
|
|
93
109
|
"""Withdraw OLAS from the service Safe to the configured withdrawal address.
|
|
@@ -132,8 +148,14 @@ class DrainManagerMixin:
|
|
|
132
148
|
return False, 0
|
|
133
149
|
|
|
134
150
|
olas_amount = olas_balance / 1e18
|
|
135
|
-
withdrawal_tag =
|
|
136
|
-
|
|
151
|
+
withdrawal_tag = (
|
|
152
|
+
self.wallet.account_service.get_tag_by_address(withdrawal_address)
|
|
153
|
+
or withdrawal_address
|
|
154
|
+
)
|
|
155
|
+
multisig_tag = (
|
|
156
|
+
self.wallet.account_service.get_tag_by_address(multisig_address)
|
|
157
|
+
or multisig_address
|
|
158
|
+
)
|
|
137
159
|
|
|
138
160
|
logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
|
|
139
161
|
|
|
@@ -183,7 +183,7 @@ class LifecycleManagerMixin:
|
|
|
183
183
|
service_name=service_name,
|
|
184
184
|
chain_name=chain_name,
|
|
185
185
|
agent_id_values=agent_id_values,
|
|
186
|
-
|
|
186
|
+
service_owner_eoa_address=service_owner_account.address,
|
|
187
187
|
token_address=token_address,
|
|
188
188
|
)
|
|
189
189
|
|
|
@@ -263,7 +263,7 @@ class LifecycleManagerMixin:
|
|
|
263
263
|
service_name: Optional[str],
|
|
264
264
|
chain_name: str,
|
|
265
265
|
agent_id_values: List[int],
|
|
266
|
-
|
|
266
|
+
service_owner_eoa_address: str,
|
|
267
267
|
token_address: Optional[str],
|
|
268
268
|
) -> None:
|
|
269
269
|
"""Create and save the new Service model."""
|
|
@@ -272,7 +272,7 @@ class LifecycleManagerMixin:
|
|
|
272
272
|
chain_name=chain_name,
|
|
273
273
|
service_id=service_id,
|
|
274
274
|
agent_ids=agent_id_values,
|
|
275
|
-
|
|
275
|
+
service_owner_eoa_address=service_owner_eoa_address,
|
|
276
276
|
token_address=token_address,
|
|
277
277
|
)
|
|
278
278
|
|
|
@@ -14,7 +14,7 @@ class TestOlasConfig:
|
|
|
14
14
|
chain_name="gnosis",
|
|
15
15
|
service_id=456,
|
|
16
16
|
agent_ids=[25],
|
|
17
|
-
|
|
17
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
config.add_service(service)
|
|
@@ -30,7 +30,7 @@ class TestOlasConfig:
|
|
|
30
30
|
chain_name="gnosis",
|
|
31
31
|
service_id=789,
|
|
32
32
|
agent_ids=[25],
|
|
33
|
-
|
|
33
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
34
34
|
)
|
|
35
35
|
config.services["gnosis:789"] = service
|
|
36
36
|
|
|
@@ -53,7 +53,7 @@ class TestOlasConfig:
|
|
|
53
53
|
chain_name="ethereum",
|
|
54
54
|
service_id=200,
|
|
55
55
|
agent_ids=[25],
|
|
56
|
-
|
|
56
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
57
57
|
)
|
|
58
58
|
config.services["ethereum:200"] = service
|
|
59
59
|
|
|
@@ -116,7 +116,7 @@ class TestService:
|
|
|
116
116
|
chain_name="gnosis",
|
|
117
117
|
service_id=123,
|
|
118
118
|
agent_ids=[25],
|
|
119
|
-
|
|
119
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
assert service.key == "gnosis:123"
|
|
@@ -133,7 +133,7 @@ class TestService:
|
|
|
133
133
|
chain_name="gnosis",
|
|
134
134
|
service_id=456,
|
|
135
135
|
agent_ids=[25],
|
|
136
|
-
|
|
136
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
137
137
|
staking_contract_address=staking_addr,
|
|
138
138
|
token_address=token_addr,
|
|
139
139
|
)
|
|
@@ -35,7 +35,7 @@ def mock_olas_config():
|
|
|
35
35
|
chain_name="gnosis",
|
|
36
36
|
agent_address=VALID_ADDR_1,
|
|
37
37
|
multisig_address=VALID_ADDR_2,
|
|
38
|
-
|
|
38
|
+
service_owner_eoa_address=VALID_ADDR_3,
|
|
39
39
|
staking_contract_address=VALID_ADDR_1,
|
|
40
40
|
)
|
|
41
41
|
config = OlasConfig(services={"gnosis:1": service})
|
|
@@ -108,6 +108,7 @@ def test_claim_rewards_no_accrued_rewards(mock_wallet):
|
|
|
108
108
|
|
|
109
109
|
mock_staking = MagicMock()
|
|
110
110
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
111
|
+
mock_staking.calculate_staking_reward.return_value = 0
|
|
111
112
|
mock_staking.get_accrued_rewards.return_value = 0
|
|
112
113
|
|
|
113
114
|
success, amount = manager.claim_rewards(staking_contract=mock_staking)
|
|
@@ -129,9 +130,12 @@ def test_claim_rewards_success(mock_wallet):
|
|
|
129
130
|
|
|
130
131
|
mock_staking = MagicMock()
|
|
131
132
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
133
|
+
mock_staking.calculate_staking_reward.return_value = 10 * 10**18 # 10 OLAS
|
|
132
134
|
mock_staking.get_accrued_rewards.return_value = 10 * 10**18 # 10 OLAS
|
|
133
135
|
mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
|
|
134
|
-
mock_staking.extract_events.return_value = [
|
|
136
|
+
mock_staking.extract_events.return_value = [
|
|
137
|
+
{"name": "RewardClaimed", "args": {"amount": 10 * 10**18}}
|
|
138
|
+
]
|
|
135
139
|
|
|
136
140
|
success, amount = manager.claim_rewards(staking_contract=mock_staking)
|
|
137
141
|
|
|
@@ -152,6 +156,7 @@ def test_claim_rewards_tx_fails(mock_wallet):
|
|
|
152
156
|
|
|
153
157
|
mock_staking = MagicMock()
|
|
154
158
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
159
|
+
mock_staking.calculate_staking_reward.return_value = 10 * 10**18
|
|
155
160
|
mock_staking.get_accrued_rewards.return_value = 10 * 10**18
|
|
156
161
|
mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
|
|
157
162
|
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
@@ -201,6 +201,7 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
|
|
|
201
201
|
mock_staking = MagicMock()
|
|
202
202
|
mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
|
|
203
203
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
204
|
+
mock_staking.calculate_staking_reward.return_value = 100
|
|
204
205
|
mock_staking.get_accrued_rewards.return_value = 100
|
|
205
206
|
|
|
206
207
|
def get_contract_side_effect(cls, *args, **kwargs):
|
|
@@ -43,7 +43,7 @@ def mock_olas_config():
|
|
|
43
43
|
chain_name="gnosis",
|
|
44
44
|
agent_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
45
45
|
multisig_address="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
|
|
46
|
-
|
|
46
|
+
service_owner_eoa_address="0x1111111111111111111111111111111111111111",
|
|
47
47
|
staking_contract_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
48
48
|
)
|
|
49
49
|
return OlasConfig(services={"gnosis:1": service})
|
|
@@ -189,6 +189,7 @@ src/tests/test_chain_interface_coverage.py
|
|
|
189
189
|
src/tests/test_chainlist_enrichment.py
|
|
190
190
|
src/tests/test_cli.py
|
|
191
191
|
src/tests/test_contract.py
|
|
192
|
+
src/tests/test_contract_cache.py
|
|
192
193
|
src/tests/test_db.py
|
|
193
194
|
src/tests/test_drain_coverage.py
|
|
194
195
|
src/tests/test_erc20.py
|