iwa 0.0.60__tar.gz → 0.0.61__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.60/src/iwa.egg-info → iwa-0.0.61}/PKG-INFO +1 -1
- {iwa-0.0.60 → iwa-0.0.61}/pyproject.toml +2 -2
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/interface.py +62 -1
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/manager.py +8 -0
- iwa-0.0.61/src/iwa/core/chainlist.py +299 -0
- {iwa-0.0.60 → iwa-0.0.61/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa.egg-info/SOURCES.txt +1 -0
- iwa-0.0.61/src/tests/test_chainlist_enrichment.py +354 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_safe_executor.py +208 -0
- iwa-0.0.61/src/tests/test_transaction_service.py +355 -0
- iwa-0.0.60/src/iwa/core/chainlist.py +0 -121
- iwa-0.0.60/src/tests/test_transaction_service.py +0 -179
- {iwa-0.0.60 → iwa-0.0.61}/LICENSE +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/README.md +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/setup.cfg +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/__main__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/cache.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/contract.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/decoder.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/db.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/http.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/rpc_monitor.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/safe.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/safe_executor.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/test.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tests/test_gnosis_fee.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tests/test_ipfs.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tests/test_pricing.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tests/test_regression_fixes.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/types.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/activity_checker.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/contracts/staking.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/events.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/importer.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/plugin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/lifecycle.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/service_manager/staking.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_archiving.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_integration.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_service_staking.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/drain_accounts.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/test_chainlist.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/server.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_chain.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_cli.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_contract.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_db.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_keys.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_main.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_migration.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_modals.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_models.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rate_limiter_retry.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rpc_efficiency.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rpc_rate_limit.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_safe_integration.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_tables.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_utils.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tests/test_workers.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.60 → iwa-0.0.61}/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.61"
|
|
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.61"
|
|
76
76
|
fix = true
|
|
77
77
|
|
|
78
78
|
[tool.ruff.lint]
|
|
@@ -54,9 +54,41 @@ class ChainInterface:
|
|
|
54
54
|
|
|
55
55
|
self._initial_block = 0
|
|
56
56
|
self._rotation_lock = threading.Lock()
|
|
57
|
-
self._session =
|
|
57
|
+
self._session = self._create_session()
|
|
58
|
+
|
|
59
|
+
# Enrich with public RPCs from ChainList (skip for Tenderly vNets)
|
|
60
|
+
if not self.is_tenderly:
|
|
61
|
+
self._enrich_rpcs_from_chainlist()
|
|
62
|
+
|
|
58
63
|
self._init_web3()
|
|
59
64
|
|
|
65
|
+
def _create_session(self) -> requests.Session:
|
|
66
|
+
"""Create a requests Session with bounded connection pooling.
|
|
67
|
+
|
|
68
|
+
Configures the session with limited pool sizes to prevent file
|
|
69
|
+
descriptor exhaustion during RPC rotations. Connections are reused
|
|
70
|
+
within the pool but won't accumulate unboundedly.
|
|
71
|
+
"""
|
|
72
|
+
session = requests.Session()
|
|
73
|
+
# Limit pool size: we only talk to one RPC at a time, but may rotate
|
|
74
|
+
# through multiple during the session lifetime. Keep modest limits.
|
|
75
|
+
adapter = requests.adapters.HTTPAdapter(
|
|
76
|
+
pool_connections=5, # Max different hosts to keep connections to
|
|
77
|
+
pool_maxsize=10, # Max connections per host
|
|
78
|
+
)
|
|
79
|
+
session.mount("https://", adapter)
|
|
80
|
+
session.mount("http://", adapter)
|
|
81
|
+
return session
|
|
82
|
+
|
|
83
|
+
def close(self) -> None:
|
|
84
|
+
"""Close the session and release all connections.
|
|
85
|
+
|
|
86
|
+
Call this when the ChainInterface is no longer needed to ensure
|
|
87
|
+
proper cleanup of network resources.
|
|
88
|
+
"""
|
|
89
|
+
if hasattr(self, "_session") and self._session:
|
|
90
|
+
self._session.close()
|
|
91
|
+
|
|
60
92
|
@property
|
|
61
93
|
def current_rpc(self) -> str:
|
|
62
94
|
"""Get the current active RPC URL."""
|
|
@@ -251,6 +283,35 @@ class ChainInterface:
|
|
|
251
283
|
]
|
|
252
284
|
return any(signal in err_text for signal in quota_signals)
|
|
253
285
|
|
|
286
|
+
# -- ChainList enrichment ----------------------------------------------
|
|
287
|
+
|
|
288
|
+
MAX_RPCS = 10 # Cap total RPCs per chain
|
|
289
|
+
|
|
290
|
+
def _enrich_rpcs_from_chainlist(self) -> None:
|
|
291
|
+
"""Add validated public RPCs from ChainList to the rotation pool."""
|
|
292
|
+
if len(self.chain.rpcs) >= self.MAX_RPCS:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
from iwa.core.chainlist import ChainlistRPC
|
|
297
|
+
|
|
298
|
+
chainlist = ChainlistRPC()
|
|
299
|
+
extra = chainlist.get_validated_rpcs(
|
|
300
|
+
self.chain.chain_id,
|
|
301
|
+
existing_rpcs=self.chain.rpcs,
|
|
302
|
+
max_results=self.MAX_RPCS - len(self.chain.rpcs),
|
|
303
|
+
)
|
|
304
|
+
if extra:
|
|
305
|
+
self.chain.rpcs.extend(extra)
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Enriched {self.chain.name} with {len(extra)} "
|
|
308
|
+
f"ChainList RPCs (total: {len(self.chain.rpcs)})"
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"ChainList enrichment failed for {self.chain.name}: {e}"
|
|
313
|
+
)
|
|
314
|
+
|
|
254
315
|
# -- Per-RPC health tracking ------------------------------------------
|
|
255
316
|
|
|
256
317
|
def _mark_rpc_backoff(self, index: int, seconds: float) -> None:
|
|
@@ -36,3 +36,11 @@ class ChainInterfaces:
|
|
|
36
36
|
for name, interface in self.items():
|
|
37
37
|
results[name] = interface.check_rpc_health()
|
|
38
38
|
return results
|
|
39
|
+
|
|
40
|
+
def close_all(self) -> None:
|
|
41
|
+
"""Close all chain interface sessions.
|
|
42
|
+
|
|
43
|
+
Call this at application shutdown to release network resources.
|
|
44
|
+
"""
|
|
45
|
+
for _, interface in self.items():
|
|
46
|
+
interface.close()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Module for fetching and parsing RPCs from Chainlist.org."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from iwa.core.constants import CACHE_DIR
|
|
12
|
+
from iwa.core.utils import configure_logger
|
|
13
|
+
|
|
14
|
+
logger = configure_logger()
|
|
15
|
+
|
|
16
|
+
# -- RPC probing constants --------------------------------------------------
|
|
17
|
+
|
|
18
|
+
MAX_CHAINLIST_CANDIDATES = 15 # Probe at most this many candidates
|
|
19
|
+
PROBE_TIMEOUT = 5.0 # Seconds per probe request
|
|
20
|
+
MAX_BLOCK_LAG = 10 # Blocks behind majority → considered stale
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalize_url(url: str) -> str:
|
|
24
|
+
"""Normalize an RPC URL for deduplication (lowercase, strip trailing slash)."""
|
|
25
|
+
return url.rstrip("/").lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_template_url(url: str) -> bool:
|
|
29
|
+
"""Return True if the URL contains template variables requiring an API key."""
|
|
30
|
+
return "${" in url or "{" in url
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def probe_rpc(
|
|
34
|
+
url: str,
|
|
35
|
+
timeout: float = PROBE_TIMEOUT,
|
|
36
|
+
session: Optional[requests.Session] = None,
|
|
37
|
+
) -> Optional[Tuple[str, float, int]]:
|
|
38
|
+
"""Probe an RPC endpoint with eth_blockNumber.
|
|
39
|
+
|
|
40
|
+
Returns ``(url, latency_ms, block_number)`` on success, or ``None``
|
|
41
|
+
if the endpoint is unreachable, slow, or returns invalid data.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
url: The RPC endpoint URL to probe.
|
|
45
|
+
timeout: Request timeout in seconds.
|
|
46
|
+
session: Optional requests.Session for connection reuse. If None,
|
|
47
|
+
creates a temporary session that is properly closed.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
# Use provided session or create temporary one with proper cleanup
|
|
51
|
+
own_session = session is None
|
|
52
|
+
if own_session:
|
|
53
|
+
session = requests.Session()
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
start = time.monotonic()
|
|
57
|
+
resp = session.post(
|
|
58
|
+
url,
|
|
59
|
+
json={"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1},
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
latency_ms = (time.monotonic() - start) * 1000
|
|
63
|
+
data = resp.json()
|
|
64
|
+
block_hex = data.get("result")
|
|
65
|
+
if not block_hex or not isinstance(block_hex, str) or block_hex == "0x0":
|
|
66
|
+
return None
|
|
67
|
+
return (url, latency_ms, int(block_hex, 16))
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
finally:
|
|
71
|
+
if own_session:
|
|
72
|
+
session.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _filter_candidates(
|
|
76
|
+
nodes: "List[RPCNode]",
|
|
77
|
+
existing_normalized: set,
|
|
78
|
+
) -> List[str]:
|
|
79
|
+
"""Filter ChainList nodes to usable HTTPS candidates."""
|
|
80
|
+
candidates: List[str] = []
|
|
81
|
+
for node in nodes:
|
|
82
|
+
url = node.url
|
|
83
|
+
if not url.startswith("https://"):
|
|
84
|
+
continue
|
|
85
|
+
if _is_template_url(url):
|
|
86
|
+
continue
|
|
87
|
+
if _normalize_url(url) in existing_normalized:
|
|
88
|
+
continue
|
|
89
|
+
candidates.append(url)
|
|
90
|
+
if len(candidates) >= MAX_CHAINLIST_CANDIDATES:
|
|
91
|
+
break
|
|
92
|
+
return candidates
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _probe_candidates(
|
|
96
|
+
candidates: List[str],
|
|
97
|
+
) -> List[Tuple[str, float, int]]:
|
|
98
|
+
"""Probe a list of RPC URLs in parallel, returning successful results.
|
|
99
|
+
|
|
100
|
+
Uses a shared session for all probes to enable connection pooling and
|
|
101
|
+
ensure proper cleanup of all connections when probing completes.
|
|
102
|
+
"""
|
|
103
|
+
results: List[Tuple[str, float, int]] = []
|
|
104
|
+
# Use a shared session with connection pooling for all probes
|
|
105
|
+
# This prevents FD leaks from individual probe connections
|
|
106
|
+
with requests.Session() as session:
|
|
107
|
+
# Configure connection pool size to match our max workers
|
|
108
|
+
adapter = requests.adapters.HTTPAdapter(
|
|
109
|
+
pool_connections=10,
|
|
110
|
+
pool_maxsize=10,
|
|
111
|
+
max_retries=0, # No retries - we handle failure gracefully
|
|
112
|
+
)
|
|
113
|
+
session.mount("https://", adapter)
|
|
114
|
+
session.mount("http://", adapter)
|
|
115
|
+
|
|
116
|
+
with ThreadPoolExecutor(max_workers=min(len(candidates), 10)) as pool:
|
|
117
|
+
futures = {
|
|
118
|
+
pool.submit(probe_rpc, url, PROBE_TIMEOUT, session): url
|
|
119
|
+
for url in candidates
|
|
120
|
+
}
|
|
121
|
+
for future in as_completed(futures, timeout=15):
|
|
122
|
+
try:
|
|
123
|
+
result = future.result()
|
|
124
|
+
if result is not None:
|
|
125
|
+
results.append(result)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
# Session is closed here via context manager, releasing all connections
|
|
129
|
+
return results
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _rank_and_select(
|
|
133
|
+
results: List[Tuple[str, float, int]],
|
|
134
|
+
candidates: List[str],
|
|
135
|
+
chain_id: int,
|
|
136
|
+
max_results: int,
|
|
137
|
+
) -> List[str]:
|
|
138
|
+
"""Rank probed RPCs by latency, filtering stale ones."""
|
|
139
|
+
blocks = sorted(r[2] for r in results)
|
|
140
|
+
median_block = blocks[len(blocks) // 2]
|
|
141
|
+
|
|
142
|
+
valid = [
|
|
143
|
+
(url, latency)
|
|
144
|
+
for url, latency, block in results
|
|
145
|
+
if median_block - block <= MAX_BLOCK_LAG
|
|
146
|
+
]
|
|
147
|
+
valid.sort(key=lambda x: x[1])
|
|
148
|
+
|
|
149
|
+
selected = [url for url, _ in valid[:max_results]]
|
|
150
|
+
if selected:
|
|
151
|
+
logger.info(
|
|
152
|
+
f"ChainList: validated {len(selected)}/{len(candidates)} "
|
|
153
|
+
f"candidates for chain {chain_id} "
|
|
154
|
+
f"(median block: {median_block})"
|
|
155
|
+
)
|
|
156
|
+
return selected
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class RPCNode:
|
|
161
|
+
"""Represents a single RPC node with its properties."""
|
|
162
|
+
|
|
163
|
+
url: str
|
|
164
|
+
is_working: bool
|
|
165
|
+
privacy: Optional[str] = None
|
|
166
|
+
tracking: Optional[str] = None
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def is_tracking(self) -> bool:
|
|
170
|
+
"""Returns True if the RPC is known to track user data."""
|
|
171
|
+
return self.privacy == "privacy" or self.tracking in ("limited", "yes")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ChainlistRPC:
|
|
175
|
+
"""Fetcher and parser for Chainlist RPC data."""
|
|
176
|
+
|
|
177
|
+
URL = "https://chainlist.org/rpcs.json"
|
|
178
|
+
CACHE_PATH = CACHE_DIR / "chainlist_rpcs.json"
|
|
179
|
+
CACHE_TTL = 86400 # 24 hours
|
|
180
|
+
|
|
181
|
+
def __init__(self) -> None:
|
|
182
|
+
"""Initialize the ChainlistRPC instance."""
|
|
183
|
+
self._data: List[Dict[str, Any]] = []
|
|
184
|
+
|
|
185
|
+
def fetch_data(self, force_refresh: bool = False) -> None:
|
|
186
|
+
"""Fetches the RPC data from Chainlist with local caching."""
|
|
187
|
+
# 1. Try local cache first unless force_refresh is requested
|
|
188
|
+
if not force_refresh and self.CACHE_PATH.exists():
|
|
189
|
+
try:
|
|
190
|
+
mtime = self.CACHE_PATH.stat().st_mtime
|
|
191
|
+
if time.time() - mtime < self.CACHE_TTL:
|
|
192
|
+
with self.CACHE_PATH.open("r") as f:
|
|
193
|
+
self._data = json.load(f)
|
|
194
|
+
if self._data:
|
|
195
|
+
return
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print(f"Error reading Chainlist cache: {e}")
|
|
198
|
+
|
|
199
|
+
# 2. Fetch from remote (use session context for proper cleanup)
|
|
200
|
+
try:
|
|
201
|
+
with requests.Session() as session:
|
|
202
|
+
response = session.get(self.URL, timeout=10)
|
|
203
|
+
response.raise_for_status()
|
|
204
|
+
self._data = response.json()
|
|
205
|
+
|
|
206
|
+
# 3. Update local cache
|
|
207
|
+
if self._data:
|
|
208
|
+
self.CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
with self.CACHE_PATH.open("w") as f:
|
|
210
|
+
json.dump(self._data, f)
|
|
211
|
+
except requests.RequestException as e:
|
|
212
|
+
print(f"Error fetching Chainlist data from {self.URL}: {e}")
|
|
213
|
+
# Fallback to expired cache if available
|
|
214
|
+
if not self._data and self.CACHE_PATH.exists():
|
|
215
|
+
try:
|
|
216
|
+
with self.CACHE_PATH.open("r") as f:
|
|
217
|
+
self._data = json.load(f)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
if not self._data:
|
|
221
|
+
self._data = []
|
|
222
|
+
|
|
223
|
+
def get_chain_data(self, chain_id: int) -> Optional[Dict[str, Any]]:
|
|
224
|
+
"""Returns the raw chain data for a specific chain ID."""
|
|
225
|
+
if not self._data:
|
|
226
|
+
self.fetch_data()
|
|
227
|
+
|
|
228
|
+
for entry in self._data:
|
|
229
|
+
if entry.get("chainId") == chain_id:
|
|
230
|
+
return entry
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
def get_rpcs(self, chain_id: int) -> List[RPCNode]:
|
|
234
|
+
"""Returns a list of RPCNode objects for a parsed and cleaner view."""
|
|
235
|
+
chain_data = self.get_chain_data(chain_id)
|
|
236
|
+
if not chain_data:
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
raw_rpcs = chain_data.get("rpc", [])
|
|
240
|
+
nodes = []
|
|
241
|
+
for rpc in raw_rpcs:
|
|
242
|
+
nodes.append(
|
|
243
|
+
RPCNode(
|
|
244
|
+
url=rpc.get("url", ""),
|
|
245
|
+
is_working=True,
|
|
246
|
+
privacy=rpc.get("privacy"),
|
|
247
|
+
tracking=rpc.get("tracking"),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return nodes
|
|
251
|
+
|
|
252
|
+
def get_https_rpcs(self, chain_id: int) -> List[str]:
|
|
253
|
+
"""Returns a list of HTTPS RPC URLs for the given chain."""
|
|
254
|
+
rpcs = self.get_rpcs(chain_id)
|
|
255
|
+
return [
|
|
256
|
+
node.url
|
|
257
|
+
for node in rpcs
|
|
258
|
+
if node.url.startswith("https://") or node.url.startswith("http://")
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
def get_wss_rpcs(self, chain_id: int) -> List[str]:
|
|
262
|
+
"""Returns a list of WSS RPC URLs for the given chain."""
|
|
263
|
+
rpcs = self.get_rpcs(chain_id)
|
|
264
|
+
return [
|
|
265
|
+
node.url
|
|
266
|
+
for node in rpcs
|
|
267
|
+
if node.url.startswith("wss://") or node.url.startswith("ws://")
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
def get_validated_rpcs(
|
|
271
|
+
self,
|
|
272
|
+
chain_id: int,
|
|
273
|
+
existing_rpcs: List[str],
|
|
274
|
+
max_results: int = 5,
|
|
275
|
+
) -> List[str]:
|
|
276
|
+
"""Return ChainList RPCs filtered, probed, and sorted by quality.
|
|
277
|
+
|
|
278
|
+
1. Fetch HTTPS RPCs from ChainList for *chain_id*.
|
|
279
|
+
2. Filter out template URLs, duplicates of *existing_rpcs*, and
|
|
280
|
+
websocket endpoints.
|
|
281
|
+
3. Probe the top candidates in parallel with ``eth_blockNumber``.
|
|
282
|
+
4. Discard RPCs that are stale (block number lagging behind majority).
|
|
283
|
+
5. Return up to *max_results* URLs sorted by latency (fastest first).
|
|
284
|
+
"""
|
|
285
|
+
nodes = self.get_rpcs(chain_id)
|
|
286
|
+
if not nodes:
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
existing_normalized = {_normalize_url(u) for u in existing_rpcs}
|
|
290
|
+
candidates = _filter_candidates(nodes, existing_normalized)
|
|
291
|
+
if not candidates:
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
results = _probe_candidates(candidates)
|
|
295
|
+
if not results:
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
selected = _rank_and_select(results, candidates, chain_id, max_results)
|
|
299
|
+
return selected
|
|
@@ -186,6 +186,7 @@ src/tests/test_balance_service.py
|
|
|
186
186
|
src/tests/test_chain.py
|
|
187
187
|
src/tests/test_chain_interface.py
|
|
188
188
|
src/tests/test_chain_interface_coverage.py
|
|
189
|
+
src/tests/test_chainlist_enrichment.py
|
|
189
190
|
src/tests/test_cli.py
|
|
190
191
|
src/tests/test_contract.py
|
|
191
192
|
src/tests/test_db.py
|