iwa 0.0.62__tar.gz → 0.0.65__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.62/src/iwa.egg-info → iwa-0.0.65}/PKG-INFO +1 -1
- {iwa-0.0.62 → iwa-0.0.65}/pyproject.toml +2 -2
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/__init__.py +2 -2
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/models.py +5 -22
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/drain.py +41 -12
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/lifecycle.py +3 -3
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_models.py +5 -5
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +1 -1
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +6 -1
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_staking.py +1 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/state.py +8 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/static/app.js +15 -1
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/static/index.html +1 -1
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.62 → iwa-0.0.65/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa.egg-info/SOURCES.txt +1 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_chainlist_enrichment.py +233 -0
- iwa-0.0.65/src/tests/test_contract_cache.py +253 -0
- iwa-0.0.65/src/tests/test_drain_coverage.py +442 -0
- iwa-0.0.65/src/tests/test_staking_simple.py +503 -0
- iwa-0.0.62/src/tests/test_drain_coverage.py +0 -180
- iwa-0.0.62/src/tests/test_staking_simple.py +0 -31
- {iwa-0.0.62 → iwa-0.0.65}/LICENSE +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/README.md +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/setup.cfg +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/__main__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/interface.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/chainlist.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/contract.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/decoder.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/db.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/http.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/models.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/safe.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/safe_executor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/test.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tests/test_ipfs.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tests/test_pricing.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tests/test_regression_fixes.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/types.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/importer.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/plugin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/drain_accounts.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/models.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/server.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_chain.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_cli.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_contract.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_db.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_keys.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_main.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_migration.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_modals.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_models.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rate_limiter_retry.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rpc_rate_limit.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_safe_executor.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_safe_integration.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_tables.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_utils.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tests/test_workers.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.62 → iwa-0.0.65}/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.65"
|
|
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.65"
|
|
76
76
|
fix = true
|
|
77
77
|
|
|
78
78
|
[tool.ruff.lint]
|
|
@@ -136,8 +136,8 @@ class TransferService(
|
|
|
136
136
|
|
|
137
137
|
amount_eth = float(chain_interface.web3.from_wei(amount_wei, "ether"))
|
|
138
138
|
logger.info(
|
|
139
|
-
f"Sending {amount_eth:.4f} {
|
|
140
|
-
f"from {from_address_or_tag} to {to_address_or_tag}"
|
|
139
|
+
f"Sending {amount_eth:.4f} {token_symbol} "
|
|
140
|
+
f"from {from_tag or from_address_or_tag} to {to_tag or to_address_or_tag}"
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
if is_safe:
|
|
@@ -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
|
|
@@ -72,6 +80,13 @@ class DrainManagerMixin:
|
|
|
72
80
|
logger.error("Failed to prepare claim transaction")
|
|
73
81
|
return False, 0
|
|
74
82
|
|
|
83
|
+
# Simulate transaction to catch revert before sending
|
|
84
|
+
try:
|
|
85
|
+
staking_contract.chain_interface.web3.eth.call(claim_tx)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Claim would revert, skipping: {e}")
|
|
88
|
+
return False, 0
|
|
89
|
+
|
|
75
90
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
76
91
|
claim_tx,
|
|
77
92
|
signer_address_or_tag=owner_address,
|
|
@@ -83,11 +98,19 @@ class DrainManagerMixin:
|
|
|
83
98
|
return False, 0
|
|
84
99
|
|
|
85
100
|
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
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
# Extract actual claimed amount from RewardClaimed event
|
|
103
|
+
claimed_amount = claimable_rewards # Default to estimated
|
|
104
|
+
for event in events:
|
|
105
|
+
if event["name"] == "RewardClaimed":
|
|
106
|
+
# RewardClaimed event has 'amount' or 'reward' field
|
|
107
|
+
claimed_amount = event["args"].get("amount", event["args"].get("reward", claimed_amount))
|
|
108
|
+
break
|
|
109
|
+
else:
|
|
110
|
+
logger.warning("RewardClaimed event not found, using estimated amount")
|
|
111
|
+
|
|
112
|
+
logger.info(f"Successfully claimed {claimed_amount / 1e18:.4f} OLAS rewards")
|
|
113
|
+
return True, claimed_amount
|
|
91
114
|
|
|
92
115
|
def withdraw_rewards(self) -> Tuple[bool, float]:
|
|
93
116
|
"""Withdraw OLAS from the service Safe to the configured withdrawal address.
|
|
@@ -132,8 +155,14 @@ class DrainManagerMixin:
|
|
|
132
155
|
return False, 0
|
|
133
156
|
|
|
134
157
|
olas_amount = olas_balance / 1e18
|
|
135
|
-
withdrawal_tag =
|
|
136
|
-
|
|
158
|
+
withdrawal_tag = (
|
|
159
|
+
self.wallet.account_service.get_tag_by_address(withdrawal_address)
|
|
160
|
+
or withdrawal_address
|
|
161
|
+
)
|
|
162
|
+
multisig_tag = (
|
|
163
|
+
self.wallet.account_service.get_tag_by_address(multisig_address)
|
|
164
|
+
or multisig_address
|
|
165
|
+
)
|
|
137
166
|
|
|
138
167
|
logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
|
|
139
168
|
|
|
@@ -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):
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from fastapi import APIRouter, Depends
|
|
4
4
|
|
|
5
5
|
from iwa.core.chain import ChainInterfaces
|
|
6
|
+
from iwa.core.models import Config
|
|
6
7
|
from iwa.web.dependencies import verify_auth
|
|
7
8
|
|
|
8
9
|
router = APIRouter(prefix="/api", tags=["state"])
|
|
@@ -25,12 +26,19 @@ def get_state(auth: bool = Depends(verify_auth)):
|
|
|
25
26
|
# Get token symbols from the interface (dict of symbol -> address)
|
|
26
27
|
tokens[name] = list(interface.tokens.keys())
|
|
27
28
|
|
|
29
|
+
# Get whitelist from config
|
|
30
|
+
config = Config()
|
|
31
|
+
whitelist = {}
|
|
32
|
+
if config.core and config.core.whitelist:
|
|
33
|
+
whitelist = {tag: str(addr) for tag, addr in config.core.whitelist.items()}
|
|
34
|
+
|
|
28
35
|
return {
|
|
29
36
|
"chains": chain_names,
|
|
30
37
|
"tokens": tokens,
|
|
31
38
|
"native_currencies": native_currencies,
|
|
32
39
|
"default_chain": "gnosis",
|
|
33
40
|
"testing": ChainInterfaces().gnosis.is_tenderly,
|
|
41
|
+
"whitelist": whitelist,
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
|
|
@@ -12,6 +12,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
12
12
|
olasServicesCache: {}, // { chain: [services] }
|
|
13
13
|
stakingContractsCache: null, // Cached staking contracts
|
|
14
14
|
olasPriceCache: null, // Cached OLAS price in EUR
|
|
15
|
+
whitelist: {}, // { tag: address } from config
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
// Real-time countdown updater for unstake availability
|
|
@@ -138,6 +139,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
138
139
|
state.chains = data.chains;
|
|
139
140
|
state.tokens = data.tokens;
|
|
140
141
|
state.nativeCurrencies = data.native_currencies || {};
|
|
142
|
+
state.whitelist = data.whitelist || {};
|
|
141
143
|
|
|
142
144
|
// Update status indicator for testing mode
|
|
143
145
|
const statusText = document.getElementById("status-text");
|
|
@@ -518,12 +520,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
518
520
|
)
|
|
519
521
|
.join("");
|
|
520
522
|
|
|
521
|
-
|
|
523
|
+
// Build To options: own accounts + whitelisted addresses
|
|
524
|
+
const accountOptions = state.accounts
|
|
522
525
|
.map(
|
|
523
526
|
(acc) =>
|
|
524
527
|
`<option value="${escapeHtml(acc.tag)}">${escapeHtml(acc.tag)}</option>`,
|
|
525
528
|
)
|
|
526
529
|
.join("");
|
|
530
|
+
const whitelistOptions = Object.entries(state.whitelist)
|
|
531
|
+
.map(
|
|
532
|
+
([tag, addr]) =>
|
|
533
|
+
`<option value="${escapeHtml(addr)}">${escapeHtml(tag)} (whitelist)</option>`,
|
|
534
|
+
)
|
|
535
|
+
.join("");
|
|
536
|
+
toSelect.innerHTML =
|
|
537
|
+
accountOptions +
|
|
538
|
+
(whitelistOptions
|
|
539
|
+
? `<optgroup label="Whitelist">${whitelistOptions}</optgroup>`
|
|
540
|
+
: "");
|
|
527
541
|
|
|
528
542
|
tokenSelect.innerHTML =
|
|
529
543
|
`<option value="native">${escapeHtml(nativeSymbol)}</option>` +
|
|
@@ -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
|
|
@@ -352,3 +352,236 @@ class TestEnrichFromChainlist:
|
|
|
352
352
|
|
|
353
353
|
# Already at MAX_RPCS=20, ChainlistRPC should not be called
|
|
354
354
|
mock_cl_cls.assert_not_called()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestRPCNode:
|
|
358
|
+
"""Test RPCNode dataclass."""
|
|
359
|
+
|
|
360
|
+
def test_is_tracking_privacy(self):
|
|
361
|
+
"""Test is_tracking returns True for privacy tracking."""
|
|
362
|
+
node = RPCNode(url="https://example.com", is_working=True, privacy="privacy")
|
|
363
|
+
assert node.is_tracking is True
|
|
364
|
+
|
|
365
|
+
def test_is_tracking_limited(self):
|
|
366
|
+
"""Test is_tracking returns True for limited tracking."""
|
|
367
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="limited")
|
|
368
|
+
assert node.is_tracking is True
|
|
369
|
+
|
|
370
|
+
def test_is_tracking_yes(self):
|
|
371
|
+
"""Test is_tracking returns True for explicit yes tracking."""
|
|
372
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="yes")
|
|
373
|
+
assert node.is_tracking is True
|
|
374
|
+
|
|
375
|
+
def test_is_tracking_none(self):
|
|
376
|
+
"""Test is_tracking returns False for no tracking."""
|
|
377
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="none")
|
|
378
|
+
assert node.is_tracking is False
|
|
379
|
+
|
|
380
|
+
def test_is_tracking_default(self):
|
|
381
|
+
"""Test is_tracking returns False by default."""
|
|
382
|
+
node = RPCNode(url="https://example.com", is_working=True)
|
|
383
|
+
assert node.is_tracking is False
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class TestFilterCandidates:
|
|
387
|
+
"""Test _filter_candidates function."""
|
|
388
|
+
|
|
389
|
+
def test_max_candidates_limit(self):
|
|
390
|
+
"""Test that _filter_candidates respects MAX_CHAINLIST_CANDIDATES."""
|
|
391
|
+
from iwa.core.chainlist import MAX_CHAINLIST_CANDIDATES, _filter_candidates
|
|
392
|
+
|
|
393
|
+
# Create more nodes than MAX_CHAINLIST_CANDIDATES
|
|
394
|
+
nodes = [
|
|
395
|
+
RPCNode(url=f"https://rpc{i}.example.com", is_working=True)
|
|
396
|
+
for i in range(MAX_CHAINLIST_CANDIDATES + 10)
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
result = _filter_candidates(nodes, set())
|
|
400
|
+
|
|
401
|
+
# Should be limited to MAX_CHAINLIST_CANDIDATES
|
|
402
|
+
assert len(result) == MAX_CHAINLIST_CANDIDATES
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class TestChainlistRPCFetchData:
|
|
406
|
+
"""Test ChainlistRPC.fetch_data with caching."""
|
|
407
|
+
|
|
408
|
+
def test_fetch_data_uses_cache(self, tmp_path):
|
|
409
|
+
"""Test fetch_data uses cached data when valid."""
|
|
410
|
+
import json
|
|
411
|
+
from unittest.mock import patch
|
|
412
|
+
|
|
413
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
414
|
+
cache_data = [{"chainId": 100, "name": "Test", "rpc": []}]
|
|
415
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
416
|
+
|
|
417
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
418
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session:
|
|
419
|
+
cl = ChainlistRPC()
|
|
420
|
+
cl.fetch_data()
|
|
421
|
+
|
|
422
|
+
# Should not make network request when cache is valid
|
|
423
|
+
mock_session.return_value.get.assert_not_called()
|
|
424
|
+
assert cl._data == cache_data
|
|
425
|
+
|
|
426
|
+
def test_fetch_data_force_refresh(self, tmp_path):
|
|
427
|
+
"""Test fetch_data ignores cache when force_refresh=True."""
|
|
428
|
+
import json
|
|
429
|
+
|
|
430
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
431
|
+
cache_data = [{"chainId": 100, "name": "Cached", "rpc": []}]
|
|
432
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
433
|
+
|
|
434
|
+
fresh_data = [{"chainId": 100, "name": "Fresh", "rpc": []}]
|
|
435
|
+
|
|
436
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
437
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
|
|
438
|
+
mock_session = MagicMock()
|
|
439
|
+
mock_session_cls.return_value.__enter__ = MagicMock(
|
|
440
|
+
return_value=mock_session
|
|
441
|
+
)
|
|
442
|
+
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
|
443
|
+
mock_resp = MagicMock()
|
|
444
|
+
mock_resp.json.return_value = fresh_data
|
|
445
|
+
mock_session.get.return_value = mock_resp
|
|
446
|
+
|
|
447
|
+
cl = ChainlistRPC()
|
|
448
|
+
cl.fetch_data(force_refresh=True)
|
|
449
|
+
|
|
450
|
+
mock_session.get.assert_called_once()
|
|
451
|
+
assert cl._data == fresh_data
|
|
452
|
+
|
|
453
|
+
def test_fetch_data_network_error_falls_back_to_cache(self, tmp_path):
|
|
454
|
+
"""Test fetch_data falls back to expired cache on network error."""
|
|
455
|
+
import json
|
|
456
|
+
|
|
457
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
458
|
+
cache_data = [{"chainId": 100, "name": "ExpiredCache", "rpc": []}]
|
|
459
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
460
|
+
|
|
461
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
462
|
+
# Force cache to be expired by setting CACHE_TTL to 0
|
|
463
|
+
with patch.object(ChainlistRPC, "CACHE_TTL", 0):
|
|
464
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
|
|
465
|
+
mock_session = MagicMock()
|
|
466
|
+
mock_session_cls.return_value.__enter__ = MagicMock(
|
|
467
|
+
return_value=mock_session
|
|
468
|
+
)
|
|
469
|
+
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
|
470
|
+
mock_session.get.side_effect = requests.RequestException("Network error")
|
|
471
|
+
|
|
472
|
+
cl = ChainlistRPC()
|
|
473
|
+
cl.fetch_data()
|
|
474
|
+
|
|
475
|
+
# Should fall back to expired cache
|
|
476
|
+
assert cl._data == cache_data
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class TestChainlistRPCGetRpcs:
|
|
480
|
+
"""Test ChainlistRPC.get_rpcs and related methods."""
|
|
481
|
+
|
|
482
|
+
def test_get_chain_data_no_data(self):
|
|
483
|
+
"""Test get_chain_data returns None when no data."""
|
|
484
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
485
|
+
cl = ChainlistRPC()
|
|
486
|
+
cl._data = []
|
|
487
|
+
result = cl.get_chain_data(999)
|
|
488
|
+
assert result is None
|
|
489
|
+
|
|
490
|
+
def test_get_chain_data_found(self):
|
|
491
|
+
"""Test get_chain_data returns chain data when found."""
|
|
492
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
493
|
+
cl = ChainlistRPC()
|
|
494
|
+
cl._data = [{"chainId": 100, "name": "Gnosis"}, {"chainId": 1, "name": "Ethereum"}]
|
|
495
|
+
result = cl.get_chain_data(100)
|
|
496
|
+
assert result == {"chainId": 100, "name": "Gnosis"}
|
|
497
|
+
|
|
498
|
+
def test_get_rpcs_parses_nodes(self):
|
|
499
|
+
"""Test get_rpcs parses RPC data into RPCNode objects."""
|
|
500
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
501
|
+
cl = ChainlistRPC()
|
|
502
|
+
cl._data = [
|
|
503
|
+
{
|
|
504
|
+
"chainId": 100,
|
|
505
|
+
"rpc": [
|
|
506
|
+
{"url": "https://rpc1.example.com", "privacy": "privacy"},
|
|
507
|
+
{"url": "https://rpc2.example.com", "tracking": "none"},
|
|
508
|
+
],
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
result = cl.get_rpcs(100)
|
|
512
|
+
|
|
513
|
+
assert len(result) == 2
|
|
514
|
+
assert result[0].url == "https://rpc1.example.com"
|
|
515
|
+
assert result[0].privacy == "privacy"
|
|
516
|
+
assert result[1].url == "https://rpc2.example.com"
|
|
517
|
+
assert result[1].tracking == "none"
|
|
518
|
+
|
|
519
|
+
def test_get_rpcs_chain_not_found(self):
|
|
520
|
+
"""Test get_rpcs returns empty list when chain not found."""
|
|
521
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
522
|
+
cl = ChainlistRPC()
|
|
523
|
+
cl._data = [{"chainId": 1, "rpc": []}]
|
|
524
|
+
result = cl.get_rpcs(999)
|
|
525
|
+
assert result == []
|
|
526
|
+
|
|
527
|
+
def test_get_https_rpcs(self):
|
|
528
|
+
"""Test get_https_rpcs filters to HTTPS/HTTP URLs."""
|
|
529
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
530
|
+
cl = ChainlistRPC()
|
|
531
|
+
cl._data = [
|
|
532
|
+
{
|
|
533
|
+
"chainId": 100,
|
|
534
|
+
"rpc": [
|
|
535
|
+
{"url": "https://rpc1.example.com"},
|
|
536
|
+
{"url": "http://rpc2.example.com"},
|
|
537
|
+
{"url": "wss://ws.example.com"},
|
|
538
|
+
],
|
|
539
|
+
}
|
|
540
|
+
]
|
|
541
|
+
result = cl.get_https_rpcs(100)
|
|
542
|
+
|
|
543
|
+
assert len(result) == 2
|
|
544
|
+
assert "https://rpc1.example.com" in result
|
|
545
|
+
assert "http://rpc2.example.com" in result
|
|
546
|
+
assert "wss://ws.example.com" not in result
|
|
547
|
+
|
|
548
|
+
def test_get_wss_rpcs(self):
|
|
549
|
+
"""Test get_wss_rpcs filters to WSS/WS URLs."""
|
|
550
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
551
|
+
cl = ChainlistRPC()
|
|
552
|
+
cl._data = [
|
|
553
|
+
{
|
|
554
|
+
"chainId": 100,
|
|
555
|
+
"rpc": [
|
|
556
|
+
{"url": "https://rpc1.example.com"},
|
|
557
|
+
{"url": "wss://ws.example.com"},
|
|
558
|
+
{"url": "ws://ws2.example.com"},
|
|
559
|
+
],
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
result = cl.get_wss_rpcs(100)
|
|
563
|
+
|
|
564
|
+
assert len(result) == 2
|
|
565
|
+
assert "wss://ws.example.com" in result
|
|
566
|
+
assert "ws://ws2.example.com" in result
|
|
567
|
+
assert "https://rpc1.example.com" not in result
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class TestGetValidatedRpcsEdgeCases:
|
|
571
|
+
"""Test edge cases in get_validated_rpcs."""
|
|
572
|
+
|
|
573
|
+
def _make_node(self, url):
|
|
574
|
+
return RPCNode(url=url, is_working=True)
|
|
575
|
+
|
|
576
|
+
@patch.object(ChainlistRPC, "get_rpcs")
|
|
577
|
+
def test_returns_empty_when_all_filtered(self, mock_get_rpcs):
|
|
578
|
+
"""Test returns empty list when all candidates are filtered."""
|
|
579
|
+
mock_get_rpcs.return_value = [
|
|
580
|
+
self._make_node("https://template.com/${API_KEY}"),
|
|
581
|
+
self._make_node("http://insecure.com"), # Not HTTPS
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
cl = ChainlistRPC()
|
|
585
|
+
result = cl.get_validated_rpcs(100, existing_rpcs=[])
|
|
586
|
+
|
|
587
|
+
assert result == []
|