iwa 0.0.33__tar.gz → 0.0.59__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.33/src/iwa.egg-info → iwa-0.0.59}/PKG-INFO +6 -3
- {iwa-0.0.33 → iwa-0.0.59}/README.md +4 -2
- {iwa-0.0.33 → iwa-0.0.59}/pyproject.toml +3 -2
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/interface.py +130 -11
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/models.py +15 -3
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/rate_limiter.py +48 -12
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chainlist.py +15 -10
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/cli.py +4 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/cache.py +1 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/contract.py +1 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/decoder.py +10 -4
- iwa-0.0.59/src/iwa/core/http.py +31 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/ipfs.py +21 -7
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/keys.py +65 -15
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/models.py +58 -13
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/pricing.py +10 -6
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/rpc_monitor.py +1 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/secrets.py +27 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/account.py +1 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/balance.py +0 -23
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/safe.py +72 -45
- iwa-0.0.59/src/iwa/core/services/safe_executor.py +350 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transaction.py +43 -13
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/erc20.py +14 -3
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/native.py +14 -31
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/swap.py +1 -0
- iwa-0.0.59/src/iwa/core/tests/test_gnosis_fee.py +91 -0
- iwa-0.0.59/src/iwa/core/tests/test_ipfs.py +85 -0
- iwa-0.0.59/src/iwa/core/tests/test_pricing.py +65 -0
- iwa-0.0.59/src/iwa/core/tests/test_regression_fixes.py +97 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/utils.py +2 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/wallet.py +6 -4
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/quotes.py +2 -2
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/swap.py +18 -32
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/tests/test_cow.py +19 -10
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/constants.py +15 -5
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/activity_checker.py +3 -3
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/staking.py +0 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/events.py +15 -13
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/importer.py +29 -25
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/models.py +0 -3
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/plugin.py +16 -14
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/drain.py +16 -9
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/lifecycle.py +23 -12
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/staking.py +15 -10
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
- iwa-0.0.59/src/iwa/plugins/olas/tests/test_olas_archiving.py +83 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_integration.py +49 -29
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view.py +5 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager.py +15 -17
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +6 -5
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +7 -6
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_staking.py +64 -38
- iwa-0.0.59/src/iwa/tools/drain_accounts.py +61 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/list_contracts.py +2 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/reset_env.py +2 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/test_chainlist.py +5 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/screens/wallets.py +2 -4
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/accounts.py +1 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/services.py +10 -5
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/static/app.js +21 -9
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/static/style.css +4 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/tests/test_web_endpoints.py +2 -2
- {iwa-0.0.33 → iwa-0.0.59/src/iwa.egg-info}/PKG-INFO +6 -3
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa.egg-info/SOURCES.txt +12 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa.egg-info/requires.txt +1 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_balance_service.py +0 -43
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_chain.py +13 -5
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_cli.py +2 -2
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_drain_coverage.py +12 -6
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_keys.py +23 -23
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_rate_limiter.py +2 -2
- iwa-0.0.59/src/tests/test_rate_limiter_retry.py +103 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_rpc_efficiency.py +4 -1
- iwa-0.0.59/src/tests/test_rpc_rate_limit.py +34 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_rpc_rotation.py +59 -11
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_safe_coverage.py +37 -23
- iwa-0.0.59/src/tests/test_safe_executor.py +361 -0
- iwa-0.0.59/src/tests/test_safe_integration.py +153 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_safe_service.py +1 -1
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transfer_swap_unit.py +5 -1
- iwa-0.0.33/src/tests/test_pricing.py +0 -160
- {iwa-0.0.33 → iwa-0.0.59}/LICENSE +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/setup.cfg +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/__main__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/constants.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/db.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/test.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/types.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/base.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/service_manager/mech.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tests/test_staking_validation.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/models.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/olas/staking.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/server.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_contract.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_db.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_main.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_migration.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_modals.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_models.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_tables.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_utils.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tests/test_workers.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.33 → iwa-0.0.59}/src/tools/verify_drain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iwa
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.59
|
|
4
4
|
Summary: A secure, modular, and plugin-based framework for crypto agents and ops
|
|
5
5
|
Requires-Python: <4.0,>=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -17,6 +17,7 @@ Requires-Dist: tomli-w>=1.2.0
|
|
|
17
17
|
Requires-Dist: typer>=0.19.2
|
|
18
18
|
Requires-Dist: web3>=7.13.0
|
|
19
19
|
Requires-Dist: pyyaml<7.0.0,>=6.0.3
|
|
20
|
+
Requires-Dist: ruamel.yaml>=0.18.0
|
|
20
21
|
Requires-Dist: safe-eth-py>=7.14.0
|
|
21
22
|
Requires-Dist: twine>=6.2.0
|
|
22
23
|
Requires-Dist: cowdao-cowpy>=1.0.1
|
|
@@ -82,7 +83,7 @@ iwa/
|
|
|
82
83
|
├── core/ # Core wallet functionality
|
|
83
84
|
│ ├── keys.py # KeyStorage - Encrypted key management
|
|
84
85
|
│ ├── wallet.py # Wallet - High-level interface
|
|
85
|
-
│ ├── chain
|
|
86
|
+
│ ├── chain/ # Blockchain interface with rate limiting
|
|
86
87
|
│ ├── services/ # Service layer (accounts, balances, transactions)
|
|
87
88
|
│ └── contracts/ # Contract abstractions (ERC20, Safe)
|
|
88
89
|
├── plugins/ # Protocol integrations
|
|
@@ -134,6 +135,9 @@ GNOSIS_RPC=https://rpc.gnosis.io,https://gnosis.drpc.org
|
|
|
134
135
|
ETHEREUM_RPC=https://mainnet.infura.io/v3/YOUR_KEY
|
|
135
136
|
BASE_RPC=https://mainnet.base.org
|
|
136
137
|
|
|
138
|
+
# Testing mode (default: true uses Tenderly test RPCs)
|
|
139
|
+
TESTING=false
|
|
140
|
+
|
|
137
141
|
# Optional
|
|
138
142
|
GNOSISSCAN_API_KEY=your_api_key
|
|
139
143
|
COINGECKO_API_KEY=your_api_key
|
|
@@ -150,7 +154,6 @@ just web
|
|
|
150
154
|
|
|
151
155
|
# Use CLI
|
|
152
156
|
iwa wallet list --chain gnosis
|
|
153
|
-
iwa wallet balance <address> --chain gnosis
|
|
154
157
|
```
|
|
155
158
|
|
|
156
159
|
### Running Tests
|
|
@@ -43,7 +43,7 @@ iwa/
|
|
|
43
43
|
├── core/ # Core wallet functionality
|
|
44
44
|
│ ├── keys.py # KeyStorage - Encrypted key management
|
|
45
45
|
│ ├── wallet.py # Wallet - High-level interface
|
|
46
|
-
│ ├── chain
|
|
46
|
+
│ ├── chain/ # Blockchain interface with rate limiting
|
|
47
47
|
│ ├── services/ # Service layer (accounts, balances, transactions)
|
|
48
48
|
│ └── contracts/ # Contract abstractions (ERC20, Safe)
|
|
49
49
|
├── plugins/ # Protocol integrations
|
|
@@ -95,6 +95,9 @@ GNOSIS_RPC=https://rpc.gnosis.io,https://gnosis.drpc.org
|
|
|
95
95
|
ETHEREUM_RPC=https://mainnet.infura.io/v3/YOUR_KEY
|
|
96
96
|
BASE_RPC=https://mainnet.base.org
|
|
97
97
|
|
|
98
|
+
# Testing mode (default: true uses Tenderly test RPCs)
|
|
99
|
+
TESTING=false
|
|
100
|
+
|
|
98
101
|
# Optional
|
|
99
102
|
GNOSISSCAN_API_KEY=your_api_key
|
|
100
103
|
COINGECKO_API_KEY=your_api_key
|
|
@@ -111,7 +114,6 @@ just web
|
|
|
111
114
|
|
|
112
115
|
# Use CLI
|
|
113
116
|
iwa wallet list --chain gnosis
|
|
114
|
-
iwa wallet balance <address> --chain gnosis
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
### Running Tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "iwa"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.59"
|
|
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"
|
|
@@ -17,6 +17,7 @@ dependencies = [
|
|
|
17
17
|
"typer>=0.19.2",
|
|
18
18
|
"web3>=7.13.0",
|
|
19
19
|
"pyyaml (>=6.0.3,<7.0.0)",
|
|
20
|
+
"ruamel.yaml>=0.18.0",
|
|
20
21
|
"safe-eth-py>=7.14.0",
|
|
21
22
|
"twine>=6.2.0",
|
|
22
23
|
"cowdao-cowpy>=1.0.1",
|
|
@@ -71,7 +72,7 @@ where = ["src"]
|
|
|
71
72
|
|
|
72
73
|
[tool.ruff]
|
|
73
74
|
line-length = 100
|
|
74
|
-
target-version = "0.0.
|
|
75
|
+
target-version = "0.0.59"
|
|
75
76
|
fix = true
|
|
76
77
|
|
|
77
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,7 @@ 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
|
|
26
28
|
|
|
27
29
|
chain: SupportedChain
|
|
28
30
|
|
|
@@ -34,9 +36,11 @@ class ChainInterface:
|
|
|
34
36
|
chain: SupportedChain = getattr(SupportedChains(), chain.lower())
|
|
35
37
|
|
|
36
38
|
self.chain = chain
|
|
37
|
-
|
|
39
|
+
# Enforce strict 1.0 RPS limit to prevent synchronization issues
|
|
40
|
+
self._rate_limiter = get_rate_limiter(chain.name, rate=1.0, burst=1)
|
|
38
41
|
self._current_rpc_index = 0
|
|
39
42
|
self._rpc_failure_counts: Dict[int, int] = {}
|
|
43
|
+
self._last_rotation_time = 0.0 # Monotonic timestamp of last rotation
|
|
40
44
|
|
|
41
45
|
if self.chain.rpc and self.chain.rpc.startswith("http://"):
|
|
42
46
|
logger.warning(
|
|
@@ -46,6 +50,7 @@ class ChainInterface:
|
|
|
46
50
|
|
|
47
51
|
self._initial_block = 0
|
|
48
52
|
self._rotation_lock = threading.Lock()
|
|
53
|
+
self._session = requests.Session()
|
|
49
54
|
self._init_web3()
|
|
50
55
|
|
|
51
56
|
@property
|
|
@@ -213,12 +218,24 @@ class ChainInterface:
|
|
|
213
218
|
]
|
|
214
219
|
return any(signal in err_text for signal in server_error_signals)
|
|
215
220
|
|
|
221
|
+
def _is_gas_error(self, error: Exception) -> bool:
|
|
222
|
+
"""Check if error is related to gas limits or fees."""
|
|
223
|
+
err_text = str(error).lower()
|
|
224
|
+
gas_signals = [
|
|
225
|
+
"intrinsic gas too low",
|
|
226
|
+
"feetoolow",
|
|
227
|
+
"gas limit",
|
|
228
|
+
"underpriced",
|
|
229
|
+
]
|
|
230
|
+
return any(signal in err_text for signal in gas_signals)
|
|
231
|
+
|
|
216
232
|
def _handle_rpc_error(self, error: Exception) -> Dict[str, Union[bool, int]]:
|
|
217
233
|
"""Handle RPC errors with smart rotation and retry logic."""
|
|
218
234
|
result: Dict[str, Union[bool, int]] = {
|
|
219
235
|
"is_rate_limit": self._is_rate_limit_error(error),
|
|
220
236
|
"is_connection_error": self._is_connection_error(error),
|
|
221
237
|
"is_server_error": self._is_server_error(error),
|
|
238
|
+
"is_gas_error": self._is_gas_error(error),
|
|
222
239
|
"is_tenderly_quota": self._is_tenderly_quota_exceeded(error),
|
|
223
240
|
"rotated": False,
|
|
224
241
|
"should_retry": False,
|
|
@@ -242,9 +259,11 @@ class ChainInterface:
|
|
|
242
259
|
|
|
243
260
|
if should_rotate:
|
|
244
261
|
error_type = "rate limit" if result["is_rate_limit"] else "connection"
|
|
262
|
+
# Extract the original URL from the error message for clarity
|
|
263
|
+
error_msg = str(error)
|
|
245
264
|
logger.warning(
|
|
246
265
|
f"RPC {error_type} error on {self.chain.name} "
|
|
247
|
-
f"(RPC #{self._current_rpc_index}): {
|
|
266
|
+
f"(current RPC #{self._current_rpc_index}): {error_msg}"
|
|
248
267
|
)
|
|
249
268
|
|
|
250
269
|
if self.rotate_rpc():
|
|
@@ -253,14 +272,22 @@ class ChainInterface:
|
|
|
253
272
|
logger.info(f"Rotated to RPC #{self._current_rpc_index} for {self.chain.name}")
|
|
254
273
|
else:
|
|
255
274
|
if result["is_rate_limit"]:
|
|
256
|
-
|
|
275
|
+
# Rotation was skipped (cooldown or single RPC) - still allow retry with current RPC
|
|
276
|
+
# We don't trigger backoff here because that would block ALL threads.
|
|
277
|
+
# Instead, we let the individual thread retry (which has its own exponential backoff).
|
|
257
278
|
result["should_retry"] = True
|
|
258
|
-
logger.
|
|
279
|
+
logger.info(
|
|
280
|
+
f"RPC rotation skipped, retrying with current RPC #{self._current_rpc_index}"
|
|
281
|
+
)
|
|
259
282
|
|
|
260
283
|
elif result["is_server_error"]:
|
|
261
284
|
logger.warning(f"Server error on {self.chain.name}: {error}")
|
|
262
285
|
result["should_retry"] = True
|
|
263
286
|
|
|
287
|
+
elif result["is_gas_error"]:
|
|
288
|
+
logger.warning(f"Gas/Fee error detected: {error}. Allowing retry for adjustment.")
|
|
289
|
+
result["should_retry"] = True
|
|
290
|
+
|
|
264
291
|
return result
|
|
265
292
|
|
|
266
293
|
def rotate_rpc(self) -> bool:
|
|
@@ -269,11 +296,22 @@ class ChainInterface:
|
|
|
269
296
|
if not self.chain.rpcs or len(self.chain.rpcs) <= 1:
|
|
270
297
|
return False
|
|
271
298
|
|
|
299
|
+
# Cooldown: prevent cascade rotations from in-flight requests
|
|
300
|
+
now = time.monotonic()
|
|
301
|
+
elapsed = now - self._last_rotation_time
|
|
302
|
+
if elapsed < self.ROTATION_COOLDOWN_SECONDS:
|
|
303
|
+
logger.debug(
|
|
304
|
+
f"RPC rotation skipped for {self.chain.name} (cooldown active, "
|
|
305
|
+
f"{self.ROTATION_COOLDOWN_SECONDS - elapsed:.1f}s remaining)"
|
|
306
|
+
)
|
|
307
|
+
return False
|
|
308
|
+
|
|
272
309
|
# Simple Round Robin rotation
|
|
273
310
|
self._current_rpc_index = (self._current_rpc_index + 1) % len(self.chain.rpcs)
|
|
274
311
|
# Internal call to _init_web3 already expects to be under lock if called from here,
|
|
275
312
|
# but _init_web3 itself doesn't have a lock. Let's make it consistent.
|
|
276
313
|
self._init_web3_under_lock()
|
|
314
|
+
self._last_rotation_time = now
|
|
277
315
|
|
|
278
316
|
logger.info(
|
|
279
317
|
f"Rotated RPC for {self.chain.name} to index {self._current_rpc_index}: {self.chain.rpcs[self._current_rpc_index]}"
|
|
@@ -288,7 +326,11 @@ class ChainInterface:
|
|
|
288
326
|
def _init_web3_under_lock(self):
|
|
289
327
|
"""Internal non-thread-safe web3 initialization."""
|
|
290
328
|
rpc_url = self.chain.rpcs[self._current_rpc_index] if self.chain.rpcs else ""
|
|
291
|
-
raw_web3 = Web3(
|
|
329
|
+
raw_web3 = Web3(
|
|
330
|
+
Web3.HTTPProvider(
|
|
331
|
+
rpc_url, request_kwargs={"timeout": DEFAULT_RPC_TIMEOUT}, session=self._session
|
|
332
|
+
)
|
|
333
|
+
)
|
|
292
334
|
|
|
293
335
|
# Use duck typing to check if current web3 is a RateLimitedWeb3 wrapper
|
|
294
336
|
if hasattr(self, "web3") and hasattr(self.web3, "set_backend"):
|
|
@@ -371,7 +413,9 @@ class ChainInterface:
|
|
|
371
413
|
except Exception:
|
|
372
414
|
return address[:6] + "..." + address[-4:]
|
|
373
415
|
|
|
374
|
-
def get_token_decimals(
|
|
416
|
+
def get_token_decimals(
|
|
417
|
+
self, address: EthereumAddress, fallback_to_18: bool = True
|
|
418
|
+
) -> Optional[int]:
|
|
375
419
|
"""Get token decimals for an address.
|
|
376
420
|
|
|
377
421
|
Args:
|
|
@@ -388,7 +432,15 @@ class ChainInterface:
|
|
|
388
432
|
# Use _web3 directly to ensure current provider after RPC rotation
|
|
389
433
|
contract = self.web3._web3.eth.contract(
|
|
390
434
|
address=self.web3.to_checksum_address(address),
|
|
391
|
-
abi=[
|
|
435
|
+
abi=[
|
|
436
|
+
{
|
|
437
|
+
"constant": True,
|
|
438
|
+
"inputs": [],
|
|
439
|
+
"name": "decimals",
|
|
440
|
+
"outputs": [{"type": "uint8"}],
|
|
441
|
+
"type": "function",
|
|
442
|
+
}
|
|
443
|
+
],
|
|
392
444
|
)
|
|
393
445
|
return contract.functions.decimals().call()
|
|
394
446
|
except Exception:
|
|
@@ -423,18 +475,85 @@ class ChainInterface:
|
|
|
423
475
|
return 500_000
|
|
424
476
|
|
|
425
477
|
def calculate_transaction_params(
|
|
426
|
-
self, built_method: Callable, tx_params: Dict[str, Union[str, int]]
|
|
478
|
+
self, built_method: Optional[Callable], tx_params: Dict[str, Union[str, int]]
|
|
427
479
|
) -> Dict[str, Union[str, int]]:
|
|
428
|
-
"""Calculate transaction parameters for a contract function call."""
|
|
480
|
+
"""Calculate transaction parameters for a contract function call or native transfer."""
|
|
481
|
+
# Baseline parameters
|
|
429
482
|
params = {
|
|
430
483
|
"from": tx_params["from"],
|
|
431
484
|
"value": tx_params.get("value", 0),
|
|
432
485
|
"nonce": self.web3.eth.get_transaction_count(tx_params["from"]),
|
|
433
|
-
"gas": self.estimate_gas(built_method, tx_params),
|
|
434
|
-
"gasPrice": self.web3.eth.gas_price,
|
|
435
486
|
}
|
|
487
|
+
|
|
488
|
+
# Add 'to' only for native transfers (built_method is None)
|
|
489
|
+
# Contract calls already have the target address in the contract object
|
|
490
|
+
if not built_method and "to" in tx_params:
|
|
491
|
+
params["to"] = tx_params["to"]
|
|
492
|
+
elif (
|
|
493
|
+
not built_method and "to" in params
|
|
494
|
+
): # Fallback if added to params earlier (though not here yet)
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
# Determine gas
|
|
498
|
+
if built_method:
|
|
499
|
+
# Contract function call
|
|
500
|
+
params["gas"] = self.estimate_gas(built_method, tx_params)
|
|
501
|
+
elif "gas" in tx_params:
|
|
502
|
+
# Manual gas override
|
|
503
|
+
params["gas"] = tx_params["gas"]
|
|
504
|
+
else:
|
|
505
|
+
# Native transfer - dynamic estimation
|
|
506
|
+
try:
|
|
507
|
+
# web3.eth.estimate_gas returns gas for the dict it receives
|
|
508
|
+
est_params = {"from": params["from"], "to": params["to"], "value": params["value"]}
|
|
509
|
+
# Remove None 'to' for contract creation simulation if needed, but usually send() has to
|
|
510
|
+
if not est_params["to"]:
|
|
511
|
+
est_params.pop("to")
|
|
512
|
+
|
|
513
|
+
estimated = self.web3.eth.estimate_gas(est_params)
|
|
514
|
+
# Apply 10% buffer for safety
|
|
515
|
+
params["gas"] = int(estimated * 1.1)
|
|
516
|
+
logger.debug(
|
|
517
|
+
f"[GAS] Estimated native transfer gas: {params['gas']} (raw: {estimated})"
|
|
518
|
+
)
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logger.debug(f"[GAS] Native estimation failed, fallback to 21000: {e}")
|
|
521
|
+
params["gas"] = 21_000
|
|
522
|
+
|
|
523
|
+
# Add EIP-1559 or Legacy fees
|
|
524
|
+
params.update(self.get_suggested_fees())
|
|
436
525
|
return params
|
|
437
526
|
|
|
527
|
+
def get_suggested_fees(self) -> Dict[str, int]:
|
|
528
|
+
"""Calculate suggested fees for a transaction (EIP-1559 or legacy)."""
|
|
529
|
+
try:
|
|
530
|
+
# Check for EIP-1559 support
|
|
531
|
+
latest_block = self.web3.eth.get_block("latest")
|
|
532
|
+
base_fee = latest_block.get("baseFeePerGas")
|
|
533
|
+
|
|
534
|
+
if base_fee is not None:
|
|
535
|
+
# EIP-1559 logic
|
|
536
|
+
max_priority_fee = int(self.web3.eth.max_priority_fee)
|
|
537
|
+
|
|
538
|
+
# Gnosis specific: ensure min priority fee (critical for validation)
|
|
539
|
+
if self.chain.name.lower() == "gnosis":
|
|
540
|
+
if max_priority_fee < 1:
|
|
541
|
+
max_priority_fee = 1 # Network minimum is 1 wei
|
|
542
|
+
|
|
543
|
+
# Global minimum for EIP-1559
|
|
544
|
+
if max_priority_fee < 1:
|
|
545
|
+
max_priority_fee = 1
|
|
546
|
+
|
|
547
|
+
# Buffer max_fee to handle base fee expansion
|
|
548
|
+
max_fee = int(base_fee * 1.5) + max_priority_fee
|
|
549
|
+
|
|
550
|
+
return {"maxFeePerGas": max_fee, "maxPriorityFeePerGas": max_priority_fee}
|
|
551
|
+
except Exception as e:
|
|
552
|
+
logger.debug(f"Failed to calculate EIP-1559 fees: {e}, falling back to legacy")
|
|
553
|
+
|
|
554
|
+
# Legacy fallback
|
|
555
|
+
return {"gasPrice": self.web3.eth.gas_price}
|
|
556
|
+
|
|
438
557
|
def wait_for_no_pending_tx(
|
|
439
558
|
self, from_address: EthereumAddress, max_wait_seconds: int = 60, poll_interval: float = 2.0
|
|
440
559
|
):
|
|
@@ -26,6 +26,9 @@ class SupportedChain(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
def get_token_address(self, token_address_or_name: str) -> Optional[EthereumAddress]:
|
|
28
28
|
"""Get token address"""
|
|
29
|
+
if not token_address_or_name:
|
|
30
|
+
return None
|
|
31
|
+
|
|
29
32
|
try:
|
|
30
33
|
address = EthereumAddress(token_address_or_name)
|
|
31
34
|
except Exception:
|
|
@@ -35,9 +38,18 @@ class SupportedChain(BaseModel):
|
|
|
35
38
|
return address
|
|
36
39
|
|
|
37
40
|
if address is None:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
# Try direct lookup
|
|
42
|
+
token_addr = self.tokens.get(token_address_or_name, None)
|
|
43
|
+
if token_addr:
|
|
44
|
+
return token_addr
|
|
45
|
+
|
|
46
|
+
# Try case-insensitive lookup
|
|
47
|
+
target_lower = token_address_or_name.lower()
|
|
48
|
+
for name, addr in self.tokens.items():
|
|
49
|
+
if name.lower() == target_lower:
|
|
50
|
+
return addr
|
|
51
|
+
|
|
52
|
+
return None
|
|
41
53
|
|
|
42
54
|
def get_token_name(self, token_address: str) -> Optional[str]:
|
|
43
55
|
"""Get token name from address."""
|
|
@@ -108,12 +108,11 @@ def get_rate_limiter(chain_name: str, rate: float = None, burst: int = None) ->
|
|
|
108
108
|
class RateLimitedEth:
|
|
109
109
|
"""Wrapper around web3.eth that applies rate limiting transparently."""
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
READ_METHODS = {
|
|
112
112
|
"get_balance",
|
|
113
113
|
"get_code",
|
|
114
114
|
"get_transaction_count",
|
|
115
115
|
"estimate_gas",
|
|
116
|
-
"send_raw_transaction",
|
|
117
116
|
"wait_for_transaction_receipt",
|
|
118
117
|
"get_block",
|
|
119
118
|
"get_transaction",
|
|
@@ -122,6 +121,16 @@ class RateLimitedEth:
|
|
|
122
121
|
"get_logs",
|
|
123
122
|
}
|
|
124
123
|
|
|
124
|
+
WRITE_METHODS = {
|
|
125
|
+
"send_raw_transaction",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Helper sets for efficient lookup
|
|
129
|
+
RPC_METHODS = READ_METHODS | WRITE_METHODS
|
|
130
|
+
|
|
131
|
+
DEFAULT_READ_RETRIES = 3
|
|
132
|
+
DEFAULT_READ_RETRY_DELAY = 0.5
|
|
133
|
+
|
|
125
134
|
def __init__(self, web3_eth, rate_limiter: RPCRateLimiter, chain_interface: "ChainInterface"):
|
|
126
135
|
"""Initialize RateLimitedEth wrapper."""
|
|
127
136
|
object.__setattr__(self, "_eth", web3_eth)
|
|
@@ -133,7 +142,7 @@ class RateLimitedEth:
|
|
|
133
142
|
attr = getattr(self._eth, name)
|
|
134
143
|
|
|
135
144
|
if name in self.RPC_METHODS and callable(attr):
|
|
136
|
-
return self.
|
|
145
|
+
return self._wrap_with_retry(attr, name)
|
|
137
146
|
|
|
138
147
|
return attr
|
|
139
148
|
|
|
@@ -151,23 +160,50 @@ class RateLimitedEth:
|
|
|
151
160
|
else:
|
|
152
161
|
delattr(self._eth, name)
|
|
153
162
|
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
@property
|
|
164
|
+
def block_number(self):
|
|
165
|
+
"""Get block number with retry."""
|
|
166
|
+
return self._execute_with_retry(lambda: self._eth.block_number, "block_number")
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
@property
|
|
169
|
+
def gas_price(self):
|
|
170
|
+
"""Get gas price with retry."""
|
|
171
|
+
return self._execute_with_retry(lambda: self._eth.gas_price, "gas_price")
|
|
172
|
+
|
|
173
|
+
def _wrap_with_retry(self, method, method_name):
|
|
174
|
+
"""Wrap method with rate limiting and retry for reads."""
|
|
162
175
|
|
|
163
176
|
def wrapper(*args, **kwargs):
|
|
164
177
|
if not self._rate_limiter.acquire(timeout=30.0):
|
|
165
|
-
raise TimeoutError(f"Rate limit timeout
|
|
178
|
+
raise TimeoutError(f"Rate limit timeout for {method_name}")
|
|
179
|
+
|
|
180
|
+
# Writes: no auto-retry (handled by caller or not safe)
|
|
181
|
+
if method_name in self.WRITE_METHODS:
|
|
182
|
+
return method(*args, **kwargs)
|
|
166
183
|
|
|
167
|
-
|
|
184
|
+
# Reads: with retry
|
|
185
|
+
return self._execute_with_retry(method, method_name, *args, **kwargs)
|
|
168
186
|
|
|
169
187
|
return wrapper
|
|
170
188
|
|
|
189
|
+
def _execute_with_retry(self, method, method_name, *args, **kwargs):
|
|
190
|
+
"""Execute read operation with retry logic."""
|
|
191
|
+
for attempt in range(self.DEFAULT_READ_RETRIES + 1):
|
|
192
|
+
try:
|
|
193
|
+
return method(*args, **kwargs)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
# Use chain interface to handle error (logging, rotation, etc.)
|
|
196
|
+
result = self._chain_interface._handle_rpc_error(e)
|
|
197
|
+
|
|
198
|
+
if not result["should_retry"] or attempt >= self.DEFAULT_READ_RETRIES:
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
delay = self.DEFAULT_READ_RETRY_DELAY * (2**attempt)
|
|
202
|
+
logger.debug(
|
|
203
|
+
f"{method_name} attempt {attempt + 1} failed, retrying in {delay:.1f}s..."
|
|
204
|
+
)
|
|
205
|
+
time.sleep(delay)
|
|
206
|
+
|
|
171
207
|
|
|
172
208
|
class RateLimitedWeb3:
|
|
173
209
|
"""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")
|
|
@@ -40,7 +43,7 @@ def account_create(
|
|
|
40
43
|
"""Create a new wallet account"""
|
|
41
44
|
key_storage = KeyStorage()
|
|
42
45
|
try:
|
|
43
|
-
key_storage.
|
|
46
|
+
key_storage.generate_new_account(tag)
|
|
44
47
|
except ValueError as e:
|
|
45
48
|
typer.echo(f"Error: {e}")
|
|
46
49
|
raise typer.Exit(code=1) from e
|
|
@@ -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
|