iwa 0.0.58__tar.gz → 0.0.60__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.58/src/iwa.egg-info → iwa-0.0.60}/PKG-INFO +1 -1
- {iwa-0.0.58 → iwa-0.0.60}/pyproject.toml +2 -2
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/interface.py +118 -53
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/rate_limiter.py +35 -12
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chainlist.py +15 -10
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/cli.py +3 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/cache.py +1 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/contract.py +1 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/decoder.py +10 -4
- iwa-0.0.60/src/iwa/core/http.py +31 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/ipfs.py +11 -19
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/keys.py +10 -4
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/models.py +1 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/pricing.py +3 -21
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/rpc_monitor.py +1 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/balance.py +0 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/safe.py +8 -2
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/safe_executor.py +52 -18
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transaction.py +32 -12
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/erc20.py +0 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/native.py +1 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tests/test_gnosis_fee.py +6 -2
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tests/test_ipfs.py +1 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tests/test_regression_fixes.py +3 -6
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/utils.py +2 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/wallet.py +3 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/constants.py +15 -5
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/activity_checker.py +3 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/staking.py +0 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/events.py +15 -13
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/importer.py +26 -20
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/plugin.py +16 -14
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/drain.py +1 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/lifecycle.py +9 -9
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/staking.py +11 -6
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_archiving.py +25 -15
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_integration.py +49 -29
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager.py +8 -10
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +5 -4
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +6 -5
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_staking.py +64 -38
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/drain_accounts.py +2 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/reset_env.py +2 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/test_chainlist.py +5 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/screens/wallets.py +1 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/services.py +10 -5
- {iwa-0.0.58 → iwa-0.0.60/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa.egg-info/SOURCES.txt +1 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_balance_service.py +0 -2
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_chain.py +1 -2
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_chain_interface.py +3 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rate_limiter.py +7 -5
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rate_limiter_retry.py +34 -33
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rpc_efficiency.py +4 -1
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rpc_rate_limit.py +4 -3
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rpc_rotation.py +4 -4
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_safe_executor.py +76 -50
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_safe_integration.py +11 -6
- {iwa-0.0.58 → iwa-0.0.60}/LICENSE +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/README.md +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/setup.cfg +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/__main__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/db.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/test.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tests/test_pricing.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/types.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/models.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/models.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/server.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_cli.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_contract.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_db.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_keys.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_main.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_migration.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_modals.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_models.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_tables.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_utils.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tests/test_workers.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.58 → iwa-0.0.60}/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.60"
|
|
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.60"
|
|
76
76
|
fix = true
|
|
77
77
|
|
|
78
78
|
[tool.ruff.lint]
|
|
@@ -4,6 +4,7 @@ import threading
|
|
|
4
4
|
import time
|
|
5
5
|
from typing import Callable, Dict, Optional, TypeVar, Union
|
|
6
6
|
|
|
7
|
+
import requests
|
|
7
8
|
from web3 import Web3
|
|
8
9
|
|
|
9
10
|
from iwa.core.chain.errors import TenderlyQuotaExceededError, sanitize_rpc_url
|
|
@@ -23,6 +24,12 @@ class ChainInterface:
|
|
|
23
24
|
|
|
24
25
|
DEFAULT_MAX_RETRIES = 6 # Allow trying most/all available RPCs on rate limit
|
|
25
26
|
DEFAULT_RETRY_DELAY = 1.0 # Base delay between retries (exponential backoff)
|
|
27
|
+
ROTATION_COOLDOWN_SECONDS = 2.0 # Minimum time between RPC rotations
|
|
28
|
+
|
|
29
|
+
# Per-error-type backoff durations (seconds) applied to the offending RPC.
|
|
30
|
+
RATE_LIMIT_BACKOFF = 10.0 # 429 Too Many Requests
|
|
31
|
+
QUOTA_EXCEEDED_BACKOFF = 300.0 # RPC quota exhausted (resets hourly/daily)
|
|
32
|
+
CONNECTION_ERROR_BACKOFF = 30.0 # Timeout / connection refused / DNS
|
|
26
33
|
|
|
27
34
|
chain: SupportedChain
|
|
28
35
|
|
|
@@ -34,10 +41,9 @@ class ChainInterface:
|
|
|
34
41
|
chain: SupportedChain = getattr(SupportedChains(), chain.lower())
|
|
35
42
|
|
|
36
43
|
self.chain = chain
|
|
37
|
-
|
|
38
|
-
self._rate_limiter = get_rate_limiter(chain.name, rate=1.0, burst=1)
|
|
44
|
+
self._rate_limiter = get_rate_limiter(chain.name, rate=5.0, burst=10)
|
|
39
45
|
self._current_rpc_index = 0
|
|
40
|
-
self.
|
|
46
|
+
self._rpc_backoff_until: Dict[int, float] = {} # index -> monotonic expiry
|
|
41
47
|
self._last_rotation_time = 0.0 # Monotonic timestamp of last rotation
|
|
42
48
|
|
|
43
49
|
if self.chain.rpc and self.chain.rpc.startswith("http://"):
|
|
@@ -48,6 +54,7 @@ class ChainInterface:
|
|
|
48
54
|
|
|
49
55
|
self._initial_block = 0
|
|
50
56
|
self._rotation_lock = threading.Lock()
|
|
57
|
+
self._session = requests.Session()
|
|
51
58
|
self._init_web3()
|
|
52
59
|
|
|
53
60
|
@property
|
|
@@ -226,6 +233,34 @@ class ChainInterface:
|
|
|
226
233
|
]
|
|
227
234
|
return any(signal in err_text for signal in gas_signals)
|
|
228
235
|
|
|
236
|
+
def _is_quota_exceeded_error(self, error: Exception) -> bool:
|
|
237
|
+
"""Check if the RPC's usage quota has been exhausted.
|
|
238
|
+
|
|
239
|
+
JSON-RPC code -32001 with messages like "Exceeded the quota usage"
|
|
240
|
+
indicates the provider's daily/hourly quota is spent. This is NOT
|
|
241
|
+
a transient 429 rate-limit; the RPC will reject ALL requests until
|
|
242
|
+
the quota resets, so it must be backed off for a long period.
|
|
243
|
+
"""
|
|
244
|
+
err_text = str(error).lower()
|
|
245
|
+
quota_signals = [
|
|
246
|
+
"exceeded the quota",
|
|
247
|
+
"exceeded quota",
|
|
248
|
+
"quota usage",
|
|
249
|
+
"quota exceeded",
|
|
250
|
+
"allowance exceeded",
|
|
251
|
+
]
|
|
252
|
+
return any(signal in err_text for signal in quota_signals)
|
|
253
|
+
|
|
254
|
+
# -- Per-RPC health tracking ------------------------------------------
|
|
255
|
+
|
|
256
|
+
def _mark_rpc_backoff(self, index: int, seconds: float) -> None:
|
|
257
|
+
"""Mark an RPC as temporarily unavailable for *seconds*."""
|
|
258
|
+
self._rpc_backoff_until[index] = time.monotonic() + seconds
|
|
259
|
+
|
|
260
|
+
def _is_rpc_healthy(self, index: int) -> bool:
|
|
261
|
+
"""Return True if the RPC at *index* is not in backoff."""
|
|
262
|
+
return time.monotonic() >= self._rpc_backoff_until.get(index, 0.0)
|
|
263
|
+
|
|
229
264
|
def _handle_rpc_error(self, error: Exception) -> Dict[str, Union[bool, int]]:
|
|
230
265
|
"""Handle RPC errors with smart rotation and retry logic."""
|
|
231
266
|
result: Dict[str, Union[bool, int]] = {
|
|
@@ -234,6 +269,7 @@ class ChainInterface:
|
|
|
234
269
|
"is_server_error": self._is_server_error(error),
|
|
235
270
|
"is_gas_error": self._is_gas_error(error),
|
|
236
271
|
"is_tenderly_quota": self._is_tenderly_quota_exceeded(error),
|
|
272
|
+
"is_quota_exceeded": self._is_quota_exceeded_error(error),
|
|
237
273
|
"rotated": False,
|
|
238
274
|
"should_retry": False,
|
|
239
275
|
}
|
|
@@ -248,19 +284,33 @@ class ChainInterface:
|
|
|
248
284
|
"Run 'uv run -m iwa.tools.reset_tenderly' to reset."
|
|
249
285
|
)
|
|
250
286
|
|
|
251
|
-
|
|
252
|
-
|
|
287
|
+
# Determine if we need to rotate and what backoff to apply.
|
|
288
|
+
should_rotate = (
|
|
289
|
+
result["is_rate_limit"]
|
|
290
|
+
or result["is_connection_error"]
|
|
291
|
+
or result["is_quota_exceeded"]
|
|
253
292
|
)
|
|
254
293
|
|
|
255
|
-
should_rotate = result["is_rate_limit"] or result["is_connection_error"]
|
|
256
|
-
|
|
257
294
|
if should_rotate:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
295
|
+
failed_index = self._current_rpc_index
|
|
296
|
+
|
|
297
|
+
# Apply per-RPC backoff so smart rotation skips this RPC.
|
|
298
|
+
if result["is_quota_exceeded"]:
|
|
299
|
+
error_type = "quota exceeded"
|
|
300
|
+
self._mark_rpc_backoff(failed_index, self.QUOTA_EXCEEDED_BACKOFF)
|
|
301
|
+
elif result["is_rate_limit"]:
|
|
302
|
+
error_type = "rate limit"
|
|
303
|
+
self._mark_rpc_backoff(failed_index, self.RATE_LIMIT_BACKOFF)
|
|
304
|
+
# Brief global backoff so other threads don't immediately flood
|
|
305
|
+
# the same (now backed-off) RPC before rotation takes effect.
|
|
306
|
+
self._rate_limiter.trigger_backoff(seconds=2.0)
|
|
307
|
+
else:
|
|
308
|
+
error_type = "connection"
|
|
309
|
+
self._mark_rpc_backoff(failed_index, self.CONNECTION_ERROR_BACKOFF)
|
|
310
|
+
|
|
261
311
|
logger.warning(
|
|
262
312
|
f"RPC {error_type} error on {self.chain.name} "
|
|
263
|
-
f"(
|
|
313
|
+
f"(RPC #{failed_index}): {error}"
|
|
264
314
|
)
|
|
265
315
|
|
|
266
316
|
if self.rotate_rpc():
|
|
@@ -268,14 +318,11 @@ class ChainInterface:
|
|
|
268
318
|
result["should_retry"] = True
|
|
269
319
|
logger.info(f"Rotated to RPC #{self._current_rpc_index} for {self.chain.name}")
|
|
270
320
|
else:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
logger.info(
|
|
277
|
-
f"RPC rotation skipped, retrying with current RPC #{self._current_rpc_index}"
|
|
278
|
-
)
|
|
321
|
+
# Rotation skipped (cooldown or single RPC) - still allow retry
|
|
322
|
+
result["should_retry"] = True
|
|
323
|
+
logger.info(
|
|
324
|
+
f"RPC rotation skipped, retrying with current RPC #{self._current_rpc_index}"
|
|
325
|
+
)
|
|
279
326
|
|
|
280
327
|
elif result["is_server_error"]:
|
|
281
328
|
logger.warning(f"Server error on {self.chain.name}: {error}")
|
|
@@ -288,33 +335,40 @@ class ChainInterface:
|
|
|
288
335
|
return result
|
|
289
336
|
|
|
290
337
|
def rotate_rpc(self) -> bool:
|
|
291
|
-
"""Rotate to the next
|
|
292
|
-
# Minimum time between rotations to prevent cascade rotations from parallel requests
|
|
293
|
-
# failing simultaneously
|
|
294
|
-
cooldown_seconds = 2.0
|
|
295
|
-
|
|
338
|
+
"""Rotate to the next healthy RPC, skipping those in backoff."""
|
|
296
339
|
with self._rotation_lock:
|
|
297
|
-
|
|
340
|
+
n = len(self.chain.rpcs) if self.chain.rpcs else 0
|
|
341
|
+
if n <= 1:
|
|
298
342
|
return False
|
|
299
343
|
|
|
300
344
|
# Cooldown: prevent cascade rotations from in-flight requests
|
|
301
345
|
now = time.monotonic()
|
|
302
|
-
if now - self._last_rotation_time <
|
|
303
|
-
logger.debug(
|
|
304
|
-
f"RPC rotation skipped for {self.chain.name} (cooldown active, "
|
|
305
|
-
f"{cooldown_seconds - (now - self._last_rotation_time):.1f}s remaining)"
|
|
306
|
-
)
|
|
346
|
+
if now - self._last_rotation_time < self.ROTATION_COOLDOWN_SECONDS:
|
|
307
347
|
return False
|
|
308
348
|
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
349
|
+
# Try each other RPC in round-robin order, preferring healthy ones.
|
|
350
|
+
best: Optional[int] = None
|
|
351
|
+
for offset in range(1, n):
|
|
352
|
+
candidate = (self._current_rpc_index + offset) % n
|
|
353
|
+
if self._is_rpc_healthy(candidate):
|
|
354
|
+
best = candidate
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
if best is None:
|
|
358
|
+
# All RPCs are in backoff — pick the one whose backoff expires soonest.
|
|
359
|
+
best = min(
|
|
360
|
+
(i for i in range(n) if i != self._current_rpc_index),
|
|
361
|
+
key=lambda i: self._rpc_backoff_until.get(i, 0.0),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
self._current_rpc_index = best
|
|
313
365
|
self._init_web3_under_lock()
|
|
314
366
|
self._last_rotation_time = now
|
|
315
367
|
|
|
368
|
+
healthy_tag = "" if self._is_rpc_healthy(best) else " (still in backoff)"
|
|
316
369
|
logger.info(
|
|
317
|
-
f"Rotated RPC for {self.chain.name} to index {
|
|
370
|
+
f"Rotated RPC for {self.chain.name} to index {best}: "
|
|
371
|
+
f"{self.chain.rpcs[best]}{healthy_tag}"
|
|
318
372
|
)
|
|
319
373
|
return True
|
|
320
374
|
|
|
@@ -326,7 +380,11 @@ class ChainInterface:
|
|
|
326
380
|
def _init_web3_under_lock(self):
|
|
327
381
|
"""Internal non-thread-safe web3 initialization."""
|
|
328
382
|
rpc_url = self.chain.rpcs[self._current_rpc_index] if self.chain.rpcs else ""
|
|
329
|
-
raw_web3 = Web3(
|
|
383
|
+
raw_web3 = Web3(
|
|
384
|
+
Web3.HTTPProvider(
|
|
385
|
+
rpc_url, request_kwargs={"timeout": DEFAULT_RPC_TIMEOUT}, session=self._session
|
|
386
|
+
)
|
|
387
|
+
)
|
|
330
388
|
|
|
331
389
|
# Use duck typing to check if current web3 is a RateLimitedWeb3 wrapper
|
|
332
390
|
if hasattr(self, "web3") and hasattr(self.web3, "set_backend"):
|
|
@@ -409,7 +467,9 @@ class ChainInterface:
|
|
|
409
467
|
except Exception:
|
|
410
468
|
return address[:6] + "..." + address[-4:]
|
|
411
469
|
|
|
412
|
-
def get_token_decimals(
|
|
470
|
+
def get_token_decimals(
|
|
471
|
+
self, address: EthereumAddress, fallback_to_18: bool = True
|
|
472
|
+
) -> Optional[int]:
|
|
413
473
|
"""Get token decimals for an address.
|
|
414
474
|
|
|
415
475
|
Args:
|
|
@@ -426,7 +486,15 @@ class ChainInterface:
|
|
|
426
486
|
# Use _web3 directly to ensure current provider after RPC rotation
|
|
427
487
|
contract = self.web3._web3.eth.contract(
|
|
428
488
|
address=self.web3.to_checksum_address(address),
|
|
429
|
-
abi=[
|
|
489
|
+
abi=[
|
|
490
|
+
{
|
|
491
|
+
"constant": True,
|
|
492
|
+
"inputs": [],
|
|
493
|
+
"name": "decimals",
|
|
494
|
+
"outputs": [{"type": "uint8"}],
|
|
495
|
+
"type": "function",
|
|
496
|
+
}
|
|
497
|
+
],
|
|
430
498
|
)
|
|
431
499
|
return contract.functions.decimals().call()
|
|
432
500
|
except Exception:
|
|
@@ -475,8 +543,10 @@ class ChainInterface:
|
|
|
475
543
|
# Contract calls already have the target address in the contract object
|
|
476
544
|
if not built_method and "to" in tx_params:
|
|
477
545
|
params["to"] = tx_params["to"]
|
|
478
|
-
elif
|
|
479
|
-
|
|
546
|
+
elif (
|
|
547
|
+
not built_method and "to" in params
|
|
548
|
+
): # Fallback if added to params earlier (though not here yet)
|
|
549
|
+
pass
|
|
480
550
|
|
|
481
551
|
# Determine gas
|
|
482
552
|
if built_method:
|
|
@@ -489,11 +559,7 @@ class ChainInterface:
|
|
|
489
559
|
# Native transfer - dynamic estimation
|
|
490
560
|
try:
|
|
491
561
|
# web3.eth.estimate_gas returns gas for the dict it receives
|
|
492
|
-
est_params = {
|
|
493
|
-
"from": params["from"],
|
|
494
|
-
"to": params["to"],
|
|
495
|
-
"value": params["value"]
|
|
496
|
-
}
|
|
562
|
+
est_params = {"from": params["from"], "to": params["to"], "value": params["value"]}
|
|
497
563
|
# Remove None 'to' for contract creation simulation if needed, but usually send() has to
|
|
498
564
|
if not est_params["to"]:
|
|
499
565
|
est_params.pop("to")
|
|
@@ -501,7 +567,9 @@ class ChainInterface:
|
|
|
501
567
|
estimated = self.web3.eth.estimate_gas(est_params)
|
|
502
568
|
# Apply 10% buffer for safety
|
|
503
569
|
params["gas"] = int(estimated * 1.1)
|
|
504
|
-
logger.debug(
|
|
570
|
+
logger.debug(
|
|
571
|
+
f"[GAS] Estimated native transfer gas: {params['gas']} (raw: {estimated})"
|
|
572
|
+
)
|
|
505
573
|
except Exception as e:
|
|
506
574
|
logger.debug(f"[GAS] Native estimation failed, fallback to 21000: {e}")
|
|
507
575
|
params["gas"] = 21_000
|
|
@@ -533,10 +601,7 @@ class ChainInterface:
|
|
|
533
601
|
# Buffer max_fee to handle base fee expansion
|
|
534
602
|
max_fee = int(base_fee * 1.5) + max_priority_fee
|
|
535
603
|
|
|
536
|
-
return {
|
|
537
|
-
"maxFeePerGas": max_fee,
|
|
538
|
-
"maxPriorityFeePerGas": max_priority_fee
|
|
539
|
-
}
|
|
604
|
+
return {"maxFeePerGas": max_fee, "maxPriorityFeePerGas": max_priority_fee}
|
|
540
605
|
except Exception as e:
|
|
541
606
|
logger.debug(f"Failed to calculate EIP-1559 fees: {e}, falling back to legacy")
|
|
542
607
|
|
|
@@ -572,6 +637,6 @@ class ChainInterface:
|
|
|
572
637
|
return self.chain.contracts.get(contract_name)
|
|
573
638
|
|
|
574
639
|
def reset_rpc_failure_counts(self):
|
|
575
|
-
"""Reset RPC
|
|
576
|
-
self.
|
|
577
|
-
logger.debug("Reset RPC
|
|
640
|
+
"""Reset RPC backoff tracking. Call periodically to allow retrying backed-off RPCs."""
|
|
641
|
+
self._rpc_backoff_until.clear()
|
|
642
|
+
logger.debug("Reset RPC backoff tracking")
|
|
@@ -128,9 +128,22 @@ class RateLimitedEth:
|
|
|
128
128
|
# Helper sets for efficient lookup
|
|
129
129
|
RPC_METHODS = READ_METHODS | WRITE_METHODS
|
|
130
130
|
|
|
131
|
-
DEFAULT_READ_RETRIES =
|
|
131
|
+
DEFAULT_READ_RETRIES = 1 # Keep low; ChainInterface.with_retry handles cross-RPC retries
|
|
132
132
|
DEFAULT_READ_RETRY_DELAY = 0.5
|
|
133
133
|
|
|
134
|
+
# Only retry errors that are clearly transient network issues.
|
|
135
|
+
# Rate-limit / quota / server errors propagate up to with_retry for rotation.
|
|
136
|
+
TRANSIENT_SIGNALS = (
|
|
137
|
+
"timeout",
|
|
138
|
+
"timed out",
|
|
139
|
+
"connection reset",
|
|
140
|
+
"connection refused",
|
|
141
|
+
"connection aborted",
|
|
142
|
+
"broken pipe",
|
|
143
|
+
"eof",
|
|
144
|
+
"remote end closed",
|
|
145
|
+
)
|
|
146
|
+
|
|
134
147
|
def __init__(self, web3_eth, rate_limiter: RPCRateLimiter, chain_interface: "ChainInterface"):
|
|
135
148
|
"""Initialize RateLimitedEth wrapper."""
|
|
136
149
|
object.__setattr__(self, "_eth", web3_eth)
|
|
@@ -187,29 +200,39 @@ class RateLimitedEth:
|
|
|
187
200
|
return wrapper
|
|
188
201
|
|
|
189
202
|
def _execute_with_retry(self, method, method_name, *args, **kwargs):
|
|
190
|
-
"""Execute read operation with retry
|
|
191
|
-
|
|
203
|
+
"""Execute a read operation with limited retry for transient errors.
|
|
204
|
+
|
|
205
|
+
Only connection-level failures (timeout, reset, broken pipe) are
|
|
206
|
+
retried here. Rate-limit, quota, and server errors propagate up
|
|
207
|
+
to ``ChainInterface.with_retry`` which handles RPC rotation.
|
|
208
|
+
This avoids the double-retry amplification that previously caused
|
|
209
|
+
up to 4x7 = 28 RPC requests per logical call.
|
|
210
|
+
"""
|
|
192
211
|
for attempt in range(self.DEFAULT_READ_RETRIES + 1):
|
|
193
212
|
try:
|
|
194
213
|
return method(*args, **kwargs)
|
|
195
214
|
except Exception as e:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
result = self._chain_interface._handle_rpc_error(e)
|
|
215
|
+
if attempt >= self.DEFAULT_READ_RETRIES:
|
|
216
|
+
raise
|
|
199
217
|
|
|
200
|
-
|
|
218
|
+
# Only retry clearly transient network errors.
|
|
219
|
+
err_text = str(e).lower()
|
|
220
|
+
if not any(signal in err_text for signal in self.TRANSIENT_SIGNALS):
|
|
201
221
|
raise
|
|
202
222
|
|
|
223
|
+
# Re-acquire a rate-limiter token before retrying.
|
|
224
|
+
if not self._rate_limiter.acquire(timeout=30.0):
|
|
225
|
+
raise TimeoutError(
|
|
226
|
+
f"Rate limit timeout for retry of {method_name}"
|
|
227
|
+
) from e
|
|
228
|
+
|
|
203
229
|
delay = self.DEFAULT_READ_RETRY_DELAY * (2**attempt)
|
|
204
230
|
logger.debug(
|
|
205
|
-
f"{method_name} attempt {attempt + 1} failed,
|
|
231
|
+
f"{method_name} attempt {attempt + 1} failed (transient), "
|
|
232
|
+
f"retrying in {delay:.1f}s..."
|
|
206
233
|
)
|
|
207
234
|
time.sleep(delay)
|
|
208
235
|
|
|
209
|
-
if last_error:
|
|
210
|
-
raise last_error
|
|
211
|
-
raise RuntimeError(f"{method_name} failed unexpectedly")
|
|
212
|
-
|
|
213
236
|
|
|
214
237
|
class RateLimitedWeb3:
|
|
215
238
|
"""Wrapper around Web3 instance that applies rate limiting transparently."""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Module for fetching and parsing RPCs from Chainlist.org."""
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
4
|
import time
|
|
4
5
|
from dataclasses import dataclass
|
|
@@ -78,7 +79,7 @@ class ChainlistRPC:
|
|
|
78
79
|
self.fetch_data()
|
|
79
80
|
|
|
80
81
|
for entry in self._data:
|
|
81
|
-
if entry.get(
|
|
82
|
+
if entry.get("chainId") == chain_id:
|
|
82
83
|
return entry
|
|
83
84
|
return None
|
|
84
85
|
|
|
@@ -88,22 +89,25 @@ class ChainlistRPC:
|
|
|
88
89
|
if not chain_data:
|
|
89
90
|
return []
|
|
90
91
|
|
|
91
|
-
raw_rpcs = chain_data.get(
|
|
92
|
+
raw_rpcs = chain_data.get("rpc", [])
|
|
92
93
|
nodes = []
|
|
93
94
|
for rpc in raw_rpcs:
|
|
94
|
-
nodes.append(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
nodes.append(
|
|
96
|
+
RPCNode(
|
|
97
|
+
url=rpc.get("url", ""),
|
|
98
|
+
is_working=True,
|
|
99
|
+
privacy=rpc.get("privacy"),
|
|
100
|
+
tracking=rpc.get("tracking"),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
100
103
|
return nodes
|
|
101
104
|
|
|
102
105
|
def get_https_rpcs(self, chain_id: int) -> List[str]:
|
|
103
106
|
"""Returns a list of HTTPS RPC URLs for the given chain."""
|
|
104
107
|
rpcs = self.get_rpcs(chain_id)
|
|
105
108
|
return [
|
|
106
|
-
node.url
|
|
109
|
+
node.url
|
|
110
|
+
for node in rpcs
|
|
107
111
|
if node.url.startswith("https://") or node.url.startswith("http://")
|
|
108
112
|
]
|
|
109
113
|
|
|
@@ -111,6 +115,7 @@ class ChainlistRPC:
|
|
|
111
115
|
"""Returns a list of WSS RPC URLs for the given chain."""
|
|
112
116
|
rpcs = self.get_rpcs(chain_id)
|
|
113
117
|
return [
|
|
114
|
-
node.url
|
|
118
|
+
node.url
|
|
119
|
+
for node in rpcs
|
|
115
120
|
if node.url.startswith("wss://") or node.url.startswith("ws://")
|
|
116
121
|
]
|
|
@@ -16,13 +16,16 @@ from iwa.tui.app import IwaApp
|
|
|
16
16
|
|
|
17
17
|
iwa_cli = typer.Typer(help="iwa command line interface")
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
@iwa_cli.callback()
|
|
20
21
|
def main_callback(ctx: typer.Context):
|
|
21
22
|
"""Initialize IWA CLI."""
|
|
22
23
|
# Print banner on startup
|
|
23
24
|
from iwa.core.utils import get_version, print_banner
|
|
25
|
+
|
|
24
26
|
print_banner("iwa", get_version("iwa"))
|
|
25
27
|
|
|
28
|
+
|
|
26
29
|
wallet_cli = typer.Typer(help="Manage wallet")
|
|
27
30
|
|
|
28
31
|
iwa_cli.add_typer(wallet_cli, name="wallet")
|
|
@@ -58,7 +58,7 @@ class ErrorDecoder:
|
|
|
58
58
|
# Also check core ABIs if they are in a different place
|
|
59
59
|
core_abi_path = src_root / "iwa" / "core" / "contracts" / "abis"
|
|
60
60
|
if core_abi_path.exists() and core_abi_path not in [f.parent for f in abi_files]:
|
|
61
|
-
|
|
61
|
+
abi_files.extend(list(core_abi_path.glob("*.json")))
|
|
62
62
|
|
|
63
63
|
logger.debug(f"Found {len(abi_files)} ABI files for error decoding.")
|
|
64
64
|
|
|
@@ -66,7 +66,11 @@ class ErrorDecoder:
|
|
|
66
66
|
try:
|
|
67
67
|
with open(abi_path, "r", encoding="utf-8") as f:
|
|
68
68
|
content = json.load(f)
|
|
69
|
-
abi =
|
|
69
|
+
abi = (
|
|
70
|
+
content.get("abi")
|
|
71
|
+
if isinstance(content, dict) and "abi" in content
|
|
72
|
+
else content
|
|
73
|
+
)
|
|
70
74
|
if isinstance(abi, list):
|
|
71
75
|
self._process_abi(abi, abi_path.name)
|
|
72
76
|
except Exception as e:
|
|
@@ -91,7 +95,7 @@ class ErrorDecoder:
|
|
|
91
95
|
"types": types,
|
|
92
96
|
"arg_names": names,
|
|
93
97
|
"source": source_name,
|
|
94
|
-
"signature": signature
|
|
98
|
+
"signature": signature,
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
if selector not in self._selectors:
|
|
@@ -145,7 +149,9 @@ class ErrorDecoder:
|
|
|
145
149
|
for d in self._selectors[selector]:
|
|
146
150
|
try:
|
|
147
151
|
decoded = decode(d["types"], bytes.fromhex(encoded_args))
|
|
148
|
-
args_str = ", ".join(
|
|
152
|
+
args_str = ", ".join(
|
|
153
|
+
f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False)
|
|
154
|
+
)
|
|
149
155
|
results.append((d["name"], f"{d['name']}({args_str})", d["source"]))
|
|
150
156
|
except Exception:
|
|
151
157
|
# Try next possible decoding for this selector
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared HTTP session utilities."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from requests.adapters import HTTPAdapter
|
|
5
|
+
from urllib3.util.retry import Retry
|
|
6
|
+
|
|
7
|
+
DEFAULT_RETRY_TOTAL = 3
|
|
8
|
+
DEFAULT_BACKOFF_FACTOR = 1
|
|
9
|
+
DEFAULT_STATUS_FORCELIST = [429, 500, 502, 503, 504]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_retry_session(
|
|
13
|
+
retries: int = DEFAULT_RETRY_TOTAL,
|
|
14
|
+
backoff_factor: int = DEFAULT_BACKOFF_FACTOR,
|
|
15
|
+
status_forcelist: list[int] | None = None,
|
|
16
|
+
) -> requests.Session:
|
|
17
|
+
"""Create a requests.Session with retry strategy.
|
|
18
|
+
|
|
19
|
+
Used by PriceService, IPFS, and other modules that need
|
|
20
|
+
persistent HTTP connections with automatic retry.
|
|
21
|
+
"""
|
|
22
|
+
session = requests.Session()
|
|
23
|
+
retry_strategy = Retry(
|
|
24
|
+
total=retries,
|
|
25
|
+
backoff_factor=backoff_factor,
|
|
26
|
+
status_forcelist=status_forcelist or DEFAULT_STATUS_FORCELIST,
|
|
27
|
+
)
|
|
28
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
29
|
+
session.mount("https://", adapter)
|
|
30
|
+
session.mount("http://", adapter)
|
|
31
|
+
return session
|
|
@@ -12,14 +12,15 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
|
12
12
|
import aiohttp
|
|
13
13
|
from multiformats import CID
|
|
14
14
|
|
|
15
|
+
from iwa.core.http import create_retry_session
|
|
15
16
|
from iwa.core.models import Config
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
import requests
|
|
19
20
|
|
|
20
|
-
# Global
|
|
21
|
+
# Global persistent sessions (reused across calls to prevent FD leaks)
|
|
21
22
|
_SYNC_SESSION: Optional["requests.Session"] = None
|
|
22
|
-
|
|
23
|
+
_ASYNC_SESSION: Optional[aiohttp.ClientSession] = None
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _compute_cid_v1_hex(data: bytes) -> str:
|
|
@@ -63,10 +64,13 @@ async def push_to_ipfs_async(
|
|
|
63
64
|
form = aiohttp.FormData()
|
|
64
65
|
form.add_field("file", data, filename="data", content_type="application/octet-stream")
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
global _ASYNC_SESSION
|
|
68
|
+
if _ASYNC_SESSION is None or _ASYNC_SESSION.closed:
|
|
69
|
+
_ASYNC_SESSION = aiohttp.ClientSession()
|
|
70
|
+
|
|
71
|
+
async with _ASYNC_SESSION.post(endpoint, data=form, params=params) as response:
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
result = await response.json()
|
|
70
74
|
|
|
71
75
|
cid_str = result["Hash"]
|
|
72
76
|
cid = CID.decode(cid_str)
|
|
@@ -90,22 +94,10 @@ def push_to_ipfs_sync(
|
|
|
90
94
|
:param pin: Whether to pin the content (default True).
|
|
91
95
|
:return: Tuple of (CIDv1 string, CIDv1 hex representation).
|
|
92
96
|
"""
|
|
93
|
-
import requests
|
|
94
|
-
from requests.adapters import HTTPAdapter
|
|
95
|
-
from urllib3.util.retry import Retry
|
|
96
|
-
|
|
97
97
|
global _SYNC_SESSION
|
|
98
98
|
|
|
99
99
|
if _SYNC_SESSION is None:
|
|
100
|
-
_SYNC_SESSION =
|
|
101
|
-
retry_strategy = Retry(
|
|
102
|
-
total=3,
|
|
103
|
-
backoff_factor=1,
|
|
104
|
-
status_forcelist=[429, 500, 502, 503, 504],
|
|
105
|
-
)
|
|
106
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
107
|
-
_SYNC_SESSION.mount("http://", adapter)
|
|
108
|
-
_SYNC_SESSION.mount("https://", adapter)
|
|
100
|
+
_SYNC_SESSION = create_retry_session()
|
|
109
101
|
|
|
110
102
|
url = api_url or Config().core.ipfs_api_url
|
|
111
103
|
endpoint = f"{url}/api/v0/add"
|
|
@@ -249,7 +249,7 @@ class KeyStorage(BaseModel):
|
|
|
249
249
|
|
|
250
250
|
with open(self._path, "w", encoding="utf-8") as f:
|
|
251
251
|
# Use mode='json' to ensure all types (EthereumAddress) are correctly serialized
|
|
252
|
-
json.dump(self.model_dump(mode=
|
|
252
|
+
json.dump(self.model_dump(mode="json"), f, indent=4)
|
|
253
253
|
f.flush()
|
|
254
254
|
os.fsync(f.fileno()) # Force write to disk (critical for Docker volumes)
|
|
255
255
|
|
|
@@ -376,10 +376,14 @@ class KeyStorage(BaseModel):
|
|
|
376
376
|
# Check for duplicate tags
|
|
377
377
|
for existing in self.accounts.values():
|
|
378
378
|
if existing.tag == account.tag and existing.address != account.address:
|
|
379
|
-
raise ValueError(
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"Tag '{account.tag}' is already used by address {existing.address}"
|
|
381
|
+
)
|
|
380
382
|
|
|
381
383
|
self.accounts[account.address] = account
|
|
382
|
-
logger.info(
|
|
384
|
+
logger.info(
|
|
385
|
+
f"[KeyStorage] Registering account: tag='{account.tag}', address={account.address}"
|
|
386
|
+
)
|
|
383
387
|
self.save()
|
|
384
388
|
|
|
385
389
|
def get_pending_mnemonic(self) -> Optional[str]:
|
|
@@ -460,7 +464,9 @@ class KeyStorage(BaseModel):
|
|
|
460
464
|
|
|
461
465
|
old_tag = account.tag
|
|
462
466
|
account.tag = new_tag
|
|
463
|
-
logger.info(
|
|
467
|
+
logger.info(
|
|
468
|
+
f"[KeyStorage] Renaming account: '{old_tag}' -> '{new_tag}' (address={account.address})"
|
|
469
|
+
)
|
|
464
470
|
self.save()
|
|
465
471
|
|
|
466
472
|
def _get_private_key(self, address: str) -> Optional[str]:
|