iwa 0.0.66__tar.gz → 0.1.1__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.66/src/iwa.egg-info → iwa-0.1.1}/PKG-INFO +1 -1
- {iwa-0.0.66 → iwa-0.1.1}/pyproject.toml +2 -2
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/interface.py +35 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/safe.py +9 -12
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/safe_executor.py +7 -7
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/cow/swap.py +18 -3
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/safe.py +61 -6
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/tests/test_cow.py +36 -33
- iwa-0.1.1/src/iwa/plugins/gnosis/tests/test_safe.py +187 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/importer.py +9 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/plugin.py +3 -2
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/staking.py +20 -19
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_staking.py +54 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tui/olas_view.py +4 -4
- {iwa-0.0.66 → iwa-0.1.1/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_chain_interface.py +55 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_safe_coverage.py +5 -2
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_safe_service.py +3 -0
- iwa-0.0.66/src/iwa/plugins/gnosis/tests/test_safe.py +0 -102
- {iwa-0.0.66 → iwa-0.1.1}/LICENSE +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/README.md +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/setup.cfg +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/__main__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/chainlist.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/contract.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/decoder.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/db.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/http.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/test.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tests/test_ipfs.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tests/test_pricing.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tests/test_regression_fixes.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/types.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/drain_accounts.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/server.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa.egg-info/SOURCES.txt +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_chain.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_chainlist_enrichment.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_cli.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_contract.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_contract_cache.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_db.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_keys.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_main.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_migration.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_modals.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_models.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rate_limiter_retry.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rpc_rate_limit.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_safe_executor.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_safe_integration.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_tables.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_utils.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tests/test_workers.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.66 → iwa-0.1.1}/src/tools/verify_drain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "iwa"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.1.1"
|
|
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.
|
|
75
|
+
target-version = "0.1.1"
|
|
76
76
|
fix = true
|
|
77
77
|
|
|
78
78
|
[tool.ruff.lint]
|
|
@@ -228,6 +228,7 @@ class ChainInterface:
|
|
|
228
228
|
"connect timeout",
|
|
229
229
|
"remote end closed",
|
|
230
230
|
"broken pipe",
|
|
231
|
+
# Note: "too many open files" handled separately by _is_fd_exhaustion_error
|
|
231
232
|
]
|
|
232
233
|
return any(signal in err_text for signal in connection_signals)
|
|
233
234
|
|
|
@@ -254,6 +255,21 @@ class ChainInterface:
|
|
|
254
255
|
]
|
|
255
256
|
return any(signal in err_text for signal in server_error_signals)
|
|
256
257
|
|
|
258
|
+
def _is_fd_exhaustion_error(self, error: Exception) -> bool:
|
|
259
|
+
"""Check if error is due to file descriptor exhaustion (OSError 24).
|
|
260
|
+
|
|
261
|
+
When FDs are exhausted, rotating RPCs makes things WORSE because it
|
|
262
|
+
creates more connections. Instead, we need to pause and let existing
|
|
263
|
+
connections drain before retrying.
|
|
264
|
+
"""
|
|
265
|
+
err_text = str(error).lower()
|
|
266
|
+
fd_signals = [
|
|
267
|
+
"too many open files",
|
|
268
|
+
"oserror(24",
|
|
269
|
+
"errno 24",
|
|
270
|
+
]
|
|
271
|
+
return any(signal in err_text for signal in fd_signals)
|
|
272
|
+
|
|
257
273
|
def _is_gas_error(self, error: Exception) -> bool:
|
|
258
274
|
"""Check if error is related to gas limits or fees."""
|
|
259
275
|
err_text = str(error).lower()
|
|
@@ -326,6 +342,9 @@ class ChainInterface:
|
|
|
326
342
|
"""Return True if the RPC at *index* is not in backoff."""
|
|
327
343
|
return time.monotonic() >= self._rpc_backoff_until.get(index, 0.0)
|
|
328
344
|
|
|
345
|
+
# FD exhaustion backoff: wait for connections to drain
|
|
346
|
+
FD_EXHAUSTION_BACKOFF = 60.0 # Long pause to let FDs drain
|
|
347
|
+
|
|
329
348
|
def _handle_rpc_error(self, error: Exception) -> Dict[str, Union[bool, int]]:
|
|
330
349
|
"""Handle RPC errors with smart rotation and retry logic."""
|
|
331
350
|
result: Dict[str, Union[bool, int]] = {
|
|
@@ -335,10 +354,26 @@ class ChainInterface:
|
|
|
335
354
|
"is_gas_error": self._is_gas_error(error),
|
|
336
355
|
"is_tenderly_quota": self._is_tenderly_quota_exceeded(error),
|
|
337
356
|
"is_quota_exceeded": self._is_quota_exceeded_error(error),
|
|
357
|
+
"is_fd_exhaustion": self._is_fd_exhaustion_error(error),
|
|
338
358
|
"rotated": False,
|
|
339
359
|
"should_retry": False,
|
|
340
360
|
}
|
|
341
361
|
|
|
362
|
+
# FD exhaustion: DO NOT rotate (creates more connections), just pause
|
|
363
|
+
if result["is_fd_exhaustion"]:
|
|
364
|
+
logger.error(
|
|
365
|
+
f"[{self.chain.name}] FD EXHAUSTION detected (too many open files). "
|
|
366
|
+
f"Pausing all RPCs for {int(self.FD_EXHAUSTION_BACKOFF)}s to drain connections. "
|
|
367
|
+
f"NO rotation to avoid creating more connections."
|
|
368
|
+
)
|
|
369
|
+
# Mark ALL RPCs as in backoff to prevent any activity
|
|
370
|
+
for i in range(len(self.chain.rpcs) if self.chain.rpcs else 0):
|
|
371
|
+
self._mark_rpc_backoff(i, self.FD_EXHAUSTION_BACKOFF)
|
|
372
|
+
# Trigger global rate limit backoff
|
|
373
|
+
self._rate_limiter.trigger_backoff(seconds=self.FD_EXHAUSTION_BACKOFF)
|
|
374
|
+
result["should_retry"] = True # Retry after backoff
|
|
375
|
+
return result
|
|
376
|
+
|
|
342
377
|
if result["is_tenderly_quota"]:
|
|
343
378
|
logger.error(
|
|
344
379
|
"TENDERLY QUOTA EXCEEDED! The virtual network has reached its limit. "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Safe service module."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING,
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
from safe_eth.eth import EthereumClient
|
|
@@ -35,7 +35,6 @@ class SafeService:
|
|
|
35
35
|
"""Initialize SafeService."""
|
|
36
36
|
self.key_storage = key_storage
|
|
37
37
|
self.account_service = account_service
|
|
38
|
-
self._client_cache: Dict[str, EthereumClient] = {}
|
|
39
38
|
|
|
40
39
|
def create_safe(
|
|
41
40
|
self,
|
|
@@ -100,15 +99,14 @@ class SafeService:
|
|
|
100
99
|
|
|
101
100
|
def _get_ethereum_client(self, chain_name: str) -> EthereumClient:
|
|
102
101
|
from iwa.core.chain import ChainInterfaces
|
|
102
|
+
from iwa.plugins.gnosis.safe import get_ethereum_client
|
|
103
103
|
|
|
104
104
|
# Use ChainInterface which has proper RPC rotation and parsing
|
|
105
105
|
chain_interface = ChainInterfaces().get(chain_name)
|
|
106
106
|
rpc_url = chain_interface.current_rpc
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return self._client_cache[rpc_url]
|
|
108
|
+
# Use shared cache to prevent FD exhaustion
|
|
109
|
+
return get_ethereum_client(rpc_url)
|
|
112
110
|
|
|
113
111
|
def _deploy_safe_contract(
|
|
114
112
|
self,
|
|
@@ -256,11 +254,8 @@ class SafeService:
|
|
|
256
254
|
continue
|
|
257
255
|
|
|
258
256
|
for chain in account.chains:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# Use ChainInterface which has proper RPC rotation and parsing
|
|
262
|
-
chain_interface = ChainInterfaces().get(chain)
|
|
263
|
-
ethereum_client = EthereumClient(chain_interface.current_rpc)
|
|
257
|
+
# Reuse cached EthereumClient to prevent FD exhaustion
|
|
258
|
+
ethereum_client = self._get_ethereum_client(chain)
|
|
264
259
|
|
|
265
260
|
code = ethereum_client.w3.eth.get_code(account.address)
|
|
266
261
|
|
|
@@ -375,7 +370,9 @@ class SafeService:
|
|
|
375
370
|
if not safe_account or not isinstance(safe_account, StoredSafeAccount):
|
|
376
371
|
raise ValueError(f"Safe account '{safe_address_or_tag}' not found.")
|
|
377
372
|
|
|
378
|
-
|
|
373
|
+
# Reuse cached EthereumClient to prevent FD exhaustion
|
|
374
|
+
ethereum_client = self._get_ethereum_client(chain_name)
|
|
375
|
+
safe = SafeMultisig(safe_account, chain_name, ethereum_client=ethereum_client)
|
|
379
376
|
safe_tx = safe.build_tx(
|
|
380
377
|
to=to,
|
|
381
378
|
value=value,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Safe transaction executor with retry logic and gas handling."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import TYPE_CHECKING,
|
|
4
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
|
-
from safe_eth.eth import
|
|
7
|
+
from safe_eth.eth import TxSpeed
|
|
8
8
|
from safe_eth.safe import Safe
|
|
9
9
|
from safe_eth.safe.safe_tx import SafeTx
|
|
10
10
|
|
|
@@ -56,7 +56,6 @@ class SafeTransactionExecutor:
|
|
|
56
56
|
config = Config().core
|
|
57
57
|
self.max_retries = max_retries or config.safe_tx_max_retries
|
|
58
58
|
self.gas_buffer = gas_buffer or config.safe_tx_gas_buffer
|
|
59
|
-
self._client_cache: Dict[str, EthereumClient] = {}
|
|
60
59
|
|
|
61
60
|
def execute_with_retry(
|
|
62
61
|
self,
|
|
@@ -64,7 +63,7 @@ class SafeTransactionExecutor:
|
|
|
64
63
|
safe_tx: SafeTx,
|
|
65
64
|
signer_keys: List[str],
|
|
66
65
|
operation_name: str = "safe_tx",
|
|
67
|
-
) -> Tuple[bool, str, Optional[
|
|
66
|
+
) -> Tuple[bool, str, Optional[dict]]:
|
|
68
67
|
"""Execute SafeTx with full retry mechanism.
|
|
69
68
|
|
|
70
69
|
Args:
|
|
@@ -303,10 +302,11 @@ class SafeTransactionExecutor:
|
|
|
303
302
|
|
|
304
303
|
def _recreate_safe_client(self, safe_address: str) -> Safe:
|
|
305
304
|
"""Recreate Safe with current (possibly rotated) RPC."""
|
|
305
|
+
from iwa.plugins.gnosis.safe import get_ethereum_client
|
|
306
|
+
|
|
306
307
|
rpc_url = self.chain_interface.current_rpc
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
ethereum_client = self._client_cache[rpc_url]
|
|
308
|
+
# Use shared cache to prevent FD exhaustion
|
|
309
|
+
ethereum_client = get_ethereum_client(rpc_url)
|
|
310
310
|
return Safe(safe_address, ethereum_client)
|
|
311
311
|
|
|
312
312
|
def _is_nonce_error(self, error: Exception) -> bool:
|
|
@@ -29,6 +29,20 @@ warnings.filterwarnings(
|
|
|
29
29
|
|
|
30
30
|
logger = configure_logger()
|
|
31
31
|
|
|
32
|
+
# Module-level session for connection pooling (avoids FD leak)
|
|
33
|
+
_session: requests.Session | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_session() -> requests.Session:
|
|
37
|
+
"""Get or create the module-level HTTP session."""
|
|
38
|
+
global _session
|
|
39
|
+
if _session is None:
|
|
40
|
+
from iwa.core.http import create_retry_session
|
|
41
|
+
|
|
42
|
+
_session = create_retry_session()
|
|
43
|
+
return _session
|
|
44
|
+
|
|
45
|
+
|
|
32
46
|
if TYPE_CHECKING:
|
|
33
47
|
from cowdao_cowpy.common.chains import Chain
|
|
34
48
|
from cowdao_cowpy.cow.swap import CompletedOrder
|
|
@@ -97,13 +111,14 @@ class CowSwap:
|
|
|
97
111
|
logger.info(f"Checking order status for UID: {order.uid}")
|
|
98
112
|
|
|
99
113
|
sleep_between_retries = 15
|
|
114
|
+
session = _get_session()
|
|
115
|
+
loop = asyncio.get_event_loop()
|
|
100
116
|
|
|
101
117
|
while True:
|
|
102
118
|
try:
|
|
103
|
-
# Use a thread executor for blocking
|
|
104
|
-
loop = asyncio.get_event_loop()
|
|
119
|
+
# Use a thread executor for blocking HTTP request
|
|
105
120
|
response = await loop.run_in_executor(
|
|
106
|
-
None, lambda:
|
|
121
|
+
None, lambda s=session: s.get(order.url, timeout=60)
|
|
107
122
|
)
|
|
108
123
|
except Exception as e:
|
|
109
124
|
logger.warning(f"Error checking order status: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Gnosis Safe interaction."""
|
|
2
2
|
|
|
3
|
-
from typing import Callable, Optional
|
|
3
|
+
from typing import Callable, Dict, Optional
|
|
4
4
|
|
|
5
5
|
from safe_eth.eth import EthereumClient
|
|
6
6
|
from safe_eth.eth.constants import NULL_ADDRESS
|
|
@@ -12,6 +12,46 @@ from iwa.core.utils import configure_logger
|
|
|
12
12
|
|
|
13
13
|
logger = configure_logger()
|
|
14
14
|
|
|
15
|
+
# Shared EthereumClient cache to prevent FD leaks
|
|
16
|
+
# Limited to MAX_CACHED_CLIENTS to avoid unbounded growth during RPC rotations
|
|
17
|
+
_ethereum_client_cache: Dict[str, EthereumClient] = {}
|
|
18
|
+
MAX_CACHED_CLIENTS = 3 # Keep only recent clients to limit FD usage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cleanup_old_clients(keep_url: str) -> None:
|
|
22
|
+
"""Remove oldest cached clients to stay under limit."""
|
|
23
|
+
global _ethereum_client_cache
|
|
24
|
+
while len(_ethereum_client_cache) >= MAX_CACHED_CLIENTS:
|
|
25
|
+
# Remove oldest entry (first key in dict, preserves insertion order in Python 3.7+)
|
|
26
|
+
for old_url in list(_ethereum_client_cache.keys()):
|
|
27
|
+
if old_url != keep_url:
|
|
28
|
+
old_client = _ethereum_client_cache.pop(old_url)
|
|
29
|
+
# Try to close any underlying HTTP session
|
|
30
|
+
if hasattr(old_client, "w3") and hasattr(old_client.w3, "provider"):
|
|
31
|
+
provider = old_client.w3.provider
|
|
32
|
+
if hasattr(provider, "_request_kwargs"):
|
|
33
|
+
session = provider._request_kwargs.get("session")
|
|
34
|
+
if session and hasattr(session, "close"):
|
|
35
|
+
try:
|
|
36
|
+
session.close()
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
logger.debug(f"Evicted EthereumClient for {old_url} from cache")
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_ethereum_client(rpc_url: str) -> EthereumClient:
|
|
44
|
+
"""Get a cached EthereumClient for the given RPC URL.
|
|
45
|
+
|
|
46
|
+
Reuses existing clients to prevent file descriptor exhaustion.
|
|
47
|
+
Limited to MAX_CACHED_CLIENTS to prevent unbounded cache growth
|
|
48
|
+
during RPC rotations.
|
|
49
|
+
"""
|
|
50
|
+
if rpc_url not in _ethereum_client_cache:
|
|
51
|
+
_cleanup_old_clients(rpc_url)
|
|
52
|
+
_ethereum_client_cache[rpc_url] = EthereumClient(rpc_url)
|
|
53
|
+
return _ethereum_client_cache[rpc_url]
|
|
54
|
+
|
|
15
55
|
|
|
16
56
|
class SafeMultisig:
|
|
17
57
|
"""Class to interact with Gnosis Safe multisig wallets.
|
|
@@ -20,17 +60,32 @@ class SafeMultisig:
|
|
|
20
60
|
checking owners, thresholds, and building/sending multi-signature transactions.
|
|
21
61
|
"""
|
|
22
62
|
|
|
23
|
-
def __init__(
|
|
24
|
-
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
safe_account: StoredSafeAccount,
|
|
66
|
+
chain_name: str,
|
|
67
|
+
ethereum_client: Optional[EthereumClient] = None,
|
|
68
|
+
):
|
|
69
|
+
"""Initialize the SafeMultisig instance.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
safe_account: The Safe account to interact with.
|
|
73
|
+
chain_name: The chain name (e.g., 'gnosis').
|
|
74
|
+
ethereum_client: Optional pre-existing EthereumClient.
|
|
75
|
+
If not provided, uses a shared cached client.
|
|
76
|
+
|
|
77
|
+
"""
|
|
25
78
|
# Normalize chain comparison to be case-insensitive
|
|
26
79
|
normalized_chains = [c.lower() for c in safe_account.chains]
|
|
27
80
|
if chain_name.lower() not in normalized_chains:
|
|
28
81
|
raise ValueError(f"Safe account is not deployed on chain: {chain_name}")
|
|
29
82
|
|
|
30
|
-
|
|
83
|
+
if ethereum_client is None:
|
|
84
|
+
from iwa.core.chain import ChainInterfaces
|
|
85
|
+
|
|
86
|
+
chain_interface = ChainInterfaces().get(chain_name.lower())
|
|
87
|
+
ethereum_client = get_ethereum_client(chain_interface.current_rpc)
|
|
31
88
|
|
|
32
|
-
chain_interface = ChainInterfaces().get(chain_name.lower())
|
|
33
|
-
ethereum_client = EthereumClient(chain_interface.current_rpc)
|
|
34
89
|
self.multisig = Safe(safe_account.address, ethereum_client)
|
|
35
90
|
self.ethereum_client = ethereum_client
|
|
36
91
|
|
|
@@ -175,19 +175,18 @@ async def test_check_cowswap_order_success(cowswap):
|
|
|
175
175
|
mock_order = MagicMock()
|
|
176
176
|
mock_order.url = "http://api/order"
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
178
|
+
mock_response = MagicMock()
|
|
179
|
+
mock_response.status_code = 200
|
|
180
|
+
mock_response.json.return_value = {
|
|
181
|
+
"status": "fulfilled",
|
|
182
|
+
"executedSellAmount": "100",
|
|
183
|
+
"executedBuyAmount": "90",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mock_session = MagicMock()
|
|
187
|
+
mock_session.get.return_value = mock_response
|
|
188
|
+
|
|
189
|
+
with patch("iwa.plugins.gnosis.cow.swap._get_session", return_value=mock_session):
|
|
191
190
|
result = await cowswap.check_cowswap_order(mock_order)
|
|
192
191
|
|
|
193
192
|
assert result == {
|
|
@@ -203,10 +202,14 @@ async def test_check_cowswap_order_expired(cowswap):
|
|
|
203
202
|
mock_order = MagicMock()
|
|
204
203
|
mock_order.url = "http://api/order"
|
|
205
204
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
mock_response = MagicMock()
|
|
206
|
+
mock_response.status_code = 200
|
|
207
|
+
mock_response.json.return_value = {"status": "expired"}
|
|
208
|
+
|
|
209
|
+
mock_session = MagicMock()
|
|
210
|
+
mock_session.get.return_value = mock_response
|
|
209
211
|
|
|
212
|
+
with patch("iwa.plugins.gnosis.cow.swap._get_session", return_value=mock_session):
|
|
210
213
|
result = await cowswap.check_cowswap_order(mock_order)
|
|
211
214
|
assert result is None
|
|
212
215
|
|
|
@@ -217,20 +220,20 @@ async def test_check_cowswap_order_timeout(cowswap):
|
|
|
217
220
|
mock_order = MagicMock()
|
|
218
221
|
mock_order.url = "http://api/order"
|
|
219
222
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
223
|
+
mock_response = MagicMock()
|
|
224
|
+
mock_response.status_code = 200
|
|
225
|
+
# Order is always open, validTo = 1000
|
|
226
|
+
mock_response.json.return_value = {
|
|
227
|
+
"status": "open",
|
|
228
|
+
"executedSellAmount": "0",
|
|
229
|
+
"validTo": 1000,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
mock_session = MagicMock()
|
|
233
|
+
mock_session.get.return_value = mock_response
|
|
234
|
+
|
|
235
|
+
with patch("iwa.plugins.gnosis.cow.swap._get_session", return_value=mock_session):
|
|
236
|
+
# Mock time to return >1060 (validTo + 60) to trigger timeout on first check
|
|
237
|
+
with patch("iwa.plugins.gnosis.cow.swap.time.time", return_value=1100):
|
|
238
|
+
result = await cowswap.check_cowswap_order(mock_order)
|
|
239
|
+
assert result is None
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Tests for Safe module."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.models import StoredSafeAccount
|
|
8
|
+
from iwa.plugins.gnosis.safe import (
|
|
9
|
+
MAX_CACHED_CLIENTS,
|
|
10
|
+
SafeMultisig,
|
|
11
|
+
_ethereum_client_cache,
|
|
12
|
+
get_ethereum_client,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_settings():
|
|
18
|
+
"""Mock settings."""
|
|
19
|
+
# secrets is no longer used in this module, so we don't need to patch it here
|
|
20
|
+
yield None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_safe_eth():
|
|
25
|
+
"""Mock safe_eth module."""
|
|
26
|
+
with (
|
|
27
|
+
patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client,
|
|
28
|
+
patch("iwa.plugins.gnosis.safe.Safe") as mock_safe,
|
|
29
|
+
):
|
|
30
|
+
yield mock_client, mock_safe
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def safe_account():
|
|
35
|
+
"""Mock safe account."""
|
|
36
|
+
return StoredSafeAccount(
|
|
37
|
+
address="0x1234567890123456789012345678901234567890",
|
|
38
|
+
owners=["0x1234567890123456789012345678901234567890"],
|
|
39
|
+
threshold=1,
|
|
40
|
+
chains=["gnosis"],
|
|
41
|
+
tag="mysafe",
|
|
42
|
+
signers=[],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_init(safe_account, mock_settings, mock_safe_eth):
|
|
47
|
+
"""Test initialization."""
|
|
48
|
+
with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
|
|
49
|
+
mock_ci = mock_ci_cls.return_value
|
|
50
|
+
mock_ci.get.return_value.current_rpc = "http://rpc"
|
|
51
|
+
ms = SafeMultisig(safe_account, "gnosis")
|
|
52
|
+
assert ms.multisig is not None
|
|
53
|
+
mock_safe_eth[0].assert_called_with("http://rpc") # EthereumClient init
|
|
54
|
+
mock_safe_eth[1].assert_called() # Safe init
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_init_invalid_chain(safe_account, mock_settings, mock_safe_eth):
|
|
58
|
+
"""Test initialization with invalid chain."""
|
|
59
|
+
with pytest.raises(ValueError, match="not deployed on chain"):
|
|
60
|
+
SafeMultisig(safe_account, "ethereum")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_getters(safe_account, mock_settings, mock_safe_eth):
|
|
64
|
+
"""Test safe property getters."""
|
|
65
|
+
ms = SafeMultisig(safe_account, "gnosis")
|
|
66
|
+
mock_safe_instance = mock_safe_eth[1].return_value
|
|
67
|
+
|
|
68
|
+
mock_safe_instance.retrieve_owners.return_value = ["0x1"]
|
|
69
|
+
assert ms.get_owners() == ["0x1"]
|
|
70
|
+
|
|
71
|
+
mock_safe_instance.retrieve_threshold.return_value = 2
|
|
72
|
+
assert ms.get_threshold() == 2
|
|
73
|
+
|
|
74
|
+
mock_safe_instance.retrieve_nonce.return_value = 5
|
|
75
|
+
assert ms.get_nonce() == 5
|
|
76
|
+
|
|
77
|
+
mock_safe_instance.retrieve_all_info.return_value = {"info": "test"}
|
|
78
|
+
assert ms.retrieve_all_info() == {"info": "test"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_build_tx(safe_account, mock_settings, mock_safe_eth):
|
|
82
|
+
"""Test build_multisig_tx."""
|
|
83
|
+
ms = SafeMultisig(safe_account, "gnosis")
|
|
84
|
+
mock_safe_instance = mock_safe_eth[1].return_value
|
|
85
|
+
mock_safe_instance.build_multisig_tx.return_value = "0xTx"
|
|
86
|
+
|
|
87
|
+
tx = ms.build_tx("0xTo", 100)
|
|
88
|
+
assert tx == "0xTx"
|
|
89
|
+
|
|
90
|
+
mock_safe_instance.build_multisig_tx.assert_called()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_send_tx(safe_account, mock_settings, mock_safe_eth):
|
|
94
|
+
"""Test send_multisig_tx."""
|
|
95
|
+
ms = SafeMultisig(safe_account, "gnosis")
|
|
96
|
+
|
|
97
|
+
# Mock build_tx just in case (though it delegates)
|
|
98
|
+
# Actually we can let it delegate to mock_safe_instance which returns "0xSafeTx"
|
|
99
|
+
mock_safe_instance = mock_safe_eth[1].return_value
|
|
100
|
+
mock_safe_instance.build_multisig_tx.return_value = "0xSafeTx"
|
|
101
|
+
|
|
102
|
+
callback = MagicMock(return_value="0xHash")
|
|
103
|
+
|
|
104
|
+
tx_hash = ms.send_tx("0xTo", 100, callback)
|
|
105
|
+
assert tx_hash == "0xHash"
|
|
106
|
+
|
|
107
|
+
callback.assert_called_with("0xSafeTx")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestEthereumClientCache:
|
|
111
|
+
"""Tests for EthereumClient caching to prevent FD exhaustion."""
|
|
112
|
+
|
|
113
|
+
def setup_method(self):
|
|
114
|
+
"""Clear cache before each test."""
|
|
115
|
+
_ethereum_client_cache.clear()
|
|
116
|
+
|
|
117
|
+
def test_cache_reuses_client(self):
|
|
118
|
+
"""Test that the same RPC URL returns the same cached client."""
|
|
119
|
+
with patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client_cls:
|
|
120
|
+
mock_client_cls.return_value = MagicMock()
|
|
121
|
+
|
|
122
|
+
client1 = get_ethereum_client("https://rpc1.example.com")
|
|
123
|
+
client2 = get_ethereum_client("https://rpc1.example.com")
|
|
124
|
+
|
|
125
|
+
assert client1 is client2
|
|
126
|
+
# Should only create one instance
|
|
127
|
+
assert mock_client_cls.call_count == 1
|
|
128
|
+
|
|
129
|
+
def test_cache_different_urls(self):
|
|
130
|
+
"""Test that different URLs create different clients."""
|
|
131
|
+
with patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client_cls:
|
|
132
|
+
mock_client_cls.side_effect = lambda url: MagicMock(url=url)
|
|
133
|
+
|
|
134
|
+
client1 = get_ethereum_client("https://rpc1.example.com")
|
|
135
|
+
client2 = get_ethereum_client("https://rpc2.example.com")
|
|
136
|
+
|
|
137
|
+
assert client1 is not client2
|
|
138
|
+
assert mock_client_cls.call_count == 2
|
|
139
|
+
|
|
140
|
+
def test_cache_limit_enforced(self):
|
|
141
|
+
"""Test that cache is limited to MAX_CACHED_CLIENTS."""
|
|
142
|
+
with patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client_cls:
|
|
143
|
+
# Create mock clients with closeable sessions
|
|
144
|
+
def create_mock_client(url):
|
|
145
|
+
client = MagicMock(url=url)
|
|
146
|
+
client.w3 = MagicMock()
|
|
147
|
+
client.w3.provider = MagicMock()
|
|
148
|
+
client.w3.provider._request_kwargs = {"session": MagicMock()}
|
|
149
|
+
return client
|
|
150
|
+
|
|
151
|
+
mock_client_cls.side_effect = create_mock_client
|
|
152
|
+
|
|
153
|
+
# Create more clients than the limit
|
|
154
|
+
urls = [f"https://rpc{i}.example.com" for i in range(MAX_CACHED_CLIENTS + 2)]
|
|
155
|
+
for url in urls:
|
|
156
|
+
get_ethereum_client(url)
|
|
157
|
+
|
|
158
|
+
# Cache should not exceed limit
|
|
159
|
+
assert len(_ethereum_client_cache) <= MAX_CACHED_CLIENTS
|
|
160
|
+
|
|
161
|
+
def test_cache_evicts_oldest(self):
|
|
162
|
+
"""Test that oldest entries are evicted when limit is reached."""
|
|
163
|
+
with patch("iwa.plugins.gnosis.safe.EthereumClient") as mock_client_cls:
|
|
164
|
+
|
|
165
|
+
def create_mock_client(url):
|
|
166
|
+
client = MagicMock(url=url)
|
|
167
|
+
client.w3 = MagicMock()
|
|
168
|
+
client.w3.provider = MagicMock()
|
|
169
|
+
client.w3.provider._request_kwargs = {"session": MagicMock()}
|
|
170
|
+
return client
|
|
171
|
+
|
|
172
|
+
mock_client_cls.side_effect = create_mock_client
|
|
173
|
+
|
|
174
|
+
# Fill cache to limit
|
|
175
|
+
first_url = "https://first.example.com"
|
|
176
|
+
get_ethereum_client(first_url)
|
|
177
|
+
for i in range(1, MAX_CACHED_CLIENTS):
|
|
178
|
+
get_ethereum_client(f"https://rpc{i}.example.com")
|
|
179
|
+
|
|
180
|
+
assert first_url in _ethereum_client_cache
|
|
181
|
+
|
|
182
|
+
# Add one more - should evict the first
|
|
183
|
+
get_ethereum_client("https://new.example.com")
|
|
184
|
+
|
|
185
|
+
# First URL should be evicted
|
|
186
|
+
assert first_url not in _ethereum_client_cache
|
|
187
|
+
assert "https://new.example.com" in _ethereum_client_cache
|
|
@@ -598,6 +598,15 @@ class OlasServiceImporter:
|
|
|
598
598
|
content = file_path.read_text().strip()
|
|
599
599
|
keystore = json.loads(content)
|
|
600
600
|
|
|
601
|
+
# Handle operate format: keystore is stringified inside "private_key"
|
|
602
|
+
if "private_key" in keystore and isinstance(keystore["private_key"], str):
|
|
603
|
+
try:
|
|
604
|
+
inner_keystore = json.loads(keystore["private_key"])
|
|
605
|
+
if "crypto" in inner_keystore and "address" in inner_keystore:
|
|
606
|
+
keystore = inner_keystore
|
|
607
|
+
except json.JSONDecodeError:
|
|
608
|
+
pass # Not a nested keystore, continue with original
|
|
609
|
+
|
|
601
610
|
# Validate it's a keystore
|
|
602
611
|
if "crypto" not in keystore or "address" not in keystore:
|
|
603
612
|
return None
|
|
@@ -67,10 +67,10 @@ class OlasPlugin(Plugin):
|
|
|
67
67
|
|
|
68
68
|
"""
|
|
69
69
|
try:
|
|
70
|
-
from safe_eth.eth import EthereumClient
|
|
71
70
|
from safe_eth.safe import Safe
|
|
72
71
|
|
|
73
72
|
from iwa.core.chain import ChainInterfaces
|
|
73
|
+
from iwa.plugins.gnosis.safe import get_ethereum_client
|
|
74
74
|
|
|
75
75
|
try:
|
|
76
76
|
chain_interface = ChainInterfaces().get(chain_name)
|
|
@@ -79,7 +79,8 @@ class OlasPlugin(Plugin):
|
|
|
79
79
|
except ValueError:
|
|
80
80
|
return None, None # Chain not supported/configured
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# Reuse cached EthereumClient to prevent FD exhaustion
|
|
83
|
+
ethereum_client = get_ethereum_client(chain_interface.current_rpc)
|
|
83
84
|
safe = Safe(safe_address, ethereum_client)
|
|
84
85
|
owners = safe.retrieve_owners()
|
|
85
86
|
return owners, True
|