iwa 0.0.19__tar.gz → 0.0.20__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.19/src/iwa.egg-info → iwa-0.0.20}/PKG-INFO +1 -1
- {iwa-0.0.19 → iwa-0.0.20}/pyproject.toml +2 -2
- iwa-0.0.20/src/iwa/core/chainlist.py +116 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/constants.py +1 -0
- iwa-0.0.20/src/iwa/core/contracts/cache.py +131 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/contract.py +7 -0
- iwa-0.0.20/src/iwa/core/rpc_monitor.py +60 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/activity_checker.py +63 -25
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/staking.py +115 -19
- iwa-0.0.20/src/iwa/plugins/olas/events.py +141 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/base.py +7 -2
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/lifecycle.py +30 -5
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/mech.py +9 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/staking.py +6 -2
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_integration.py +38 -10
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager.py +7 -1
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager_errors.py +22 -11
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager_flows.py +24 -8
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_staking.py +59 -15
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_staking_validation.py +8 -14
- iwa-0.0.20/src/iwa/tools/test_chainlist.py +38 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/staking.py +9 -4
- {iwa-0.0.19 → iwa-0.0.20/src/iwa.egg-info}/PKG-INFO +1 -1
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa.egg-info/SOURCES.txt +6 -0
- iwa-0.0.20/src/tests/test_rpc_efficiency.py +103 -0
- {iwa-0.0.19 → iwa-0.0.20}/LICENSE +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/README.md +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/setup.cfg +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/__main__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/errors.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/interface.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/manager.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/chain/rate_limiter.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/cli.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/abis/erc20.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/abis/multisend.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/abis/multisend_call_only.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/erc20.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/contracts/multisend.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/db.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/ipfs.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/keys.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/mnemonic.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/monitor.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/plugins.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/pricing.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/secrets.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/account.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/balance.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/plugin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/safe.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transaction.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/base.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/erc20.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/multisend.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/native.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/services/transfer/swap.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/tables.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/test.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/tests/test_wallet.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/types.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/ui.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/utils.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/core/wallet.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/cow/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/cow/quotes.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/cow/swap.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/cow/types.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/cow_utils.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/plugin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/safe.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/tests/test_cow.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/gnosis/tests/test_safe.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/constants.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/activity_checker.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/mech.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/mech_marketplace.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/mech_new.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/service_manager.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/service_registry.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/staking.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/abis/staking_token.json +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/base.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/mech.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/mech_marketplace.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/mech_marketplace_v1.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/contracts/service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/importer.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/mech_reference.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/plugin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/scripts/test_full_mech_flow.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/scripts/test_simple_lifecycle.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/service_manager/drain.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/conftest.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_importer.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_importer_error_handling.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_mech_contracts.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_contracts.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_view.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_view_actions.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_olas_view_modals.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_plugin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_plugin_full.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_lifecycle.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager_mech.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager_rewards.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_service_manager_validation.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tests/test_staking_integration.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tui/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/plugins/olas/tui/olas_view.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/check_profile.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/list_contracts.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/release.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/reset_env.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/reset_tenderly.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/restore_backup.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tools/wallet_check.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/app.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/modals/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/modals/base.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/rpc.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/screens/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/screens/wallets.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/tests/test_app.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/tests/test_rpc.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/tests/test_wallets_refactor.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/tests/test_widgets.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/widgets/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/widgets/base.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/tui/workers.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/dependencies.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/accounts.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/__init__.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/admin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/funding.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/general.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/olas/services.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/state.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/swap.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/routers/transactions.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/server.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/static/app.js +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/static/index.html +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/static/style.css +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/tests/test_web_endpoints.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/tests/test_web_olas.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/tests/test_web_swap.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa/web/tests/test_web_swap_coverage.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa.egg-info/dependency_links.txt +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa.egg-info/entry_points.txt +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa.egg-info/requires.txt +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/iwa.egg-info/top_level.txt +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_cow.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_safe.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_transaction_retry_logic.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_tui.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_wallets_screen.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/legacy_web.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_account_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_balance_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_chain.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_chain_interface.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_chain_interface_coverage.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_cli.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_contract.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_db.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_drain_coverage.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_erc20.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_gnosis_plugin.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_keys.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_legacy_wallet.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_main.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_migration.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_mnemonic.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_modals.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_models.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_monitor.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_multisend.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_plugin_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_pricing.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_rate_limiter.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_reset_tenderly.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_rpc_rotation.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_rpc_view.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_safe_coverage.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_safe_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_service_manager_integration.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_service_manager_structure.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_service_transaction.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_staking_router.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_staking_simple.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_tables.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transaction_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transfer_multisend.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transfer_native.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transfer_security.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transfer_structure.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_transfer_swap_unit.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_ui_coverage.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_utils.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tests/test_workers.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/src/tools/create_and_stake_service.py +0 -0
- {iwa-0.0.19 → iwa-0.0.20}/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.20"
|
|
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"
|
|
@@ -71,7 +71,7 @@ where = ["src"]
|
|
|
71
71
|
|
|
72
72
|
[tool.ruff]
|
|
73
73
|
line-length = 100
|
|
74
|
-
target-version = "0.0.
|
|
74
|
+
target-version = "0.0.20"
|
|
75
75
|
fix = true
|
|
76
76
|
|
|
77
77
|
[tool.ruff.lint]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Module for fetching and parsing RPCs from Chainlist.org."""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from iwa.core.constants import CACHE_DIR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RPCNode:
|
|
14
|
+
"""Represents a single RPC node with its properties."""
|
|
15
|
+
|
|
16
|
+
url: str
|
|
17
|
+
is_working: bool
|
|
18
|
+
privacy: Optional[str] = None
|
|
19
|
+
tracking: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_tracking(self) -> bool:
|
|
23
|
+
"""Returns True if the RPC is known to track user data."""
|
|
24
|
+
return self.privacy == "privacy" or self.tracking in ("limited", "yes")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChainlistRPC:
|
|
28
|
+
"""Fetcher and parser for Chainlist RPC data."""
|
|
29
|
+
|
|
30
|
+
URL = "https://chainlist.org/rpcs.json"
|
|
31
|
+
CACHE_PATH = CACHE_DIR / "chainlist_rpcs.json"
|
|
32
|
+
CACHE_TTL = 86400 # 24 hours
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Initialize the ChainlistRPC instance."""
|
|
36
|
+
self._data: List[Dict[str, Any]] = []
|
|
37
|
+
|
|
38
|
+
def fetch_data(self, force_refresh: bool = False) -> None:
|
|
39
|
+
"""Fetches the RPC data from Chainlist with local caching."""
|
|
40
|
+
# 1. Try local cache first unless force_refresh is requested
|
|
41
|
+
if not force_refresh and self.CACHE_PATH.exists():
|
|
42
|
+
try:
|
|
43
|
+
mtime = self.CACHE_PATH.stat().st_mtime
|
|
44
|
+
if time.time() - mtime < self.CACHE_TTL:
|
|
45
|
+
with self.CACHE_PATH.open("r") as f:
|
|
46
|
+
self._data = json.load(f)
|
|
47
|
+
if self._data:
|
|
48
|
+
return
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Error reading Chainlist cache: {e}")
|
|
51
|
+
|
|
52
|
+
# 2. Fetch from remote
|
|
53
|
+
try:
|
|
54
|
+
response = requests.get(self.URL, timeout=10)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
self._data = response.json()
|
|
57
|
+
|
|
58
|
+
# 3. Update local cache
|
|
59
|
+
if self._data:
|
|
60
|
+
self.CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
with self.CACHE_PATH.open("w") as f:
|
|
62
|
+
json.dump(self._data, f)
|
|
63
|
+
except requests.RequestException as e:
|
|
64
|
+
print(f"Error fetching Chainlist data from {self.URL}: {e}")
|
|
65
|
+
# Fallback to expired cache if available
|
|
66
|
+
if not self._data and self.CACHE_PATH.exists():
|
|
67
|
+
try:
|
|
68
|
+
with self.CACHE_PATH.open("r") as f:
|
|
69
|
+
self._data = json.load(f)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
if not self._data:
|
|
73
|
+
self._data = []
|
|
74
|
+
|
|
75
|
+
def get_chain_data(self, chain_id: int) -> Optional[Dict[str, Any]]:
|
|
76
|
+
"""Returns the raw chain data for a specific chain ID."""
|
|
77
|
+
if not self._data:
|
|
78
|
+
self.fetch_data()
|
|
79
|
+
|
|
80
|
+
for entry in self._data:
|
|
81
|
+
if entry.get('chainId') == chain_id:
|
|
82
|
+
return entry
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def get_rpcs(self, chain_id: int) -> List[RPCNode]:
|
|
86
|
+
"""Returns a list of RPCNode objects for a parsed and cleaner view."""
|
|
87
|
+
chain_data = self.get_chain_data(chain_id)
|
|
88
|
+
if not chain_data:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
raw_rpcs = chain_data.get('rpc', [])
|
|
92
|
+
nodes = []
|
|
93
|
+
for rpc in raw_rpcs:
|
|
94
|
+
nodes.append(RPCNode(
|
|
95
|
+
url=rpc.get('url', ''),
|
|
96
|
+
is_working=True,
|
|
97
|
+
privacy=rpc.get('privacy'),
|
|
98
|
+
tracking=rpc.get('tracking')
|
|
99
|
+
))
|
|
100
|
+
return nodes
|
|
101
|
+
|
|
102
|
+
def get_https_rpcs(self, chain_id: int) -> List[str]:
|
|
103
|
+
"""Returns a list of HTTPS RPC URLs for the given chain."""
|
|
104
|
+
rpcs = self.get_rpcs(chain_id)
|
|
105
|
+
return [
|
|
106
|
+
node.url for node in rpcs
|
|
107
|
+
if node.url.startswith("https://") or node.url.startswith("http://")
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
def get_wss_rpcs(self, chain_id: int) -> List[str]:
|
|
111
|
+
"""Returns a list of WSS RPC URLs for the given chain."""
|
|
112
|
+
rpcs = self.get_rpcs(chain_id)
|
|
113
|
+
return [
|
|
114
|
+
node.url for node in rpcs
|
|
115
|
+
if node.url.startswith("wss://") or node.url.startswith("ws://")
|
|
116
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Contract instance cache to reduce RPC calls during instantiation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Any, Dict, Optional, Type, TypeVar
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContractCache:
|
|
14
|
+
"""Singleton cache for contract instances.
|
|
15
|
+
|
|
16
|
+
Stores contract instances keyed by (class, address, chain) to prevent
|
|
17
|
+
redundant instantiation and the associated RPC calls.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_instance = None
|
|
21
|
+
_lock = Lock()
|
|
22
|
+
|
|
23
|
+
def __new__(cls) -> "ContractCache":
|
|
24
|
+
"""Ensure singleton instance."""
|
|
25
|
+
with cls._lock:
|
|
26
|
+
if cls._instance is None:
|
|
27
|
+
cls._instance = super(ContractCache, cls).__new__(cls)
|
|
28
|
+
cls._instance._contracts: Dict[str, Any] = {}
|
|
29
|
+
cls._instance._creation_times: Dict[str, float] = {}
|
|
30
|
+
|
|
31
|
+
# Default TTL: 1 hour, configurable via env var
|
|
32
|
+
env_ttl = os.environ.get("IWA_CONTRACT_CACHE_TTL")
|
|
33
|
+
try:
|
|
34
|
+
cls._instance.ttl = int(env_ttl) if env_ttl else 3600
|
|
35
|
+
except ValueError:
|
|
36
|
+
cls._instance.ttl = 3600
|
|
37
|
+
logger.warning(f"Invalid IWA_CONTRACT_CACHE_TTL value: {env_ttl}. Using 3600.")
|
|
38
|
+
|
|
39
|
+
return cls._instance
|
|
40
|
+
|
|
41
|
+
def get_contract(
|
|
42
|
+
self,
|
|
43
|
+
contract_cls: Type[T],
|
|
44
|
+
address: str,
|
|
45
|
+
chain_name: str,
|
|
46
|
+
ttl: Optional[int] = None,
|
|
47
|
+
) -> T:
|
|
48
|
+
"""Get a cached contract instance or create a new one.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
contract_cls: The contract class to instantiate.
|
|
52
|
+
address: The contract address.
|
|
53
|
+
chain_name: The chain name.
|
|
54
|
+
ttl: Optional TTL override in seconds.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The contract instance (cached or new).
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
if not address:
|
|
61
|
+
raise ValueError("Address is required for contract caching")
|
|
62
|
+
|
|
63
|
+
key = self._make_key(contract_cls, address, chain_name)
|
|
64
|
+
now = time.time()
|
|
65
|
+
expiry = (ttl if ttl is not None else self.ttl)
|
|
66
|
+
|
|
67
|
+
with self._lock:
|
|
68
|
+
# Check if cached and valid
|
|
69
|
+
if key in self._contracts:
|
|
70
|
+
created_at = self._creation_times.get(key, 0)
|
|
71
|
+
if now - created_at < expiry:
|
|
72
|
+
return self._contracts[key]
|
|
73
|
+
else:
|
|
74
|
+
logger.debug(f"Contract cache expired for {key}")
|
|
75
|
+
del self._contracts[key]
|
|
76
|
+
del self._creation_times[key]
|
|
77
|
+
|
|
78
|
+
# Create new instance
|
|
79
|
+
logger.debug(f"Creating new cached contract instance for {key}")
|
|
80
|
+
instance = contract_cls(address, chain_name=chain_name)
|
|
81
|
+
self._contracts[key] = instance
|
|
82
|
+
self._creation_times[key] = now
|
|
83
|
+
return instance
|
|
84
|
+
|
|
85
|
+
def get_if_cached(
|
|
86
|
+
self,
|
|
87
|
+
contract_cls: Type[T],
|
|
88
|
+
address: str,
|
|
89
|
+
chain_name: str,
|
|
90
|
+
) -> Optional[T]:
|
|
91
|
+
"""Get a cached contract instance if it exists and is valid.
|
|
92
|
+
|
|
93
|
+
Does NOT create a new instance if not found.
|
|
94
|
+
"""
|
|
95
|
+
if not address:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
key = self._make_key(contract_cls, address, chain_name)
|
|
99
|
+
now = time.time()
|
|
100
|
+
|
|
101
|
+
with self._lock:
|
|
102
|
+
if key in self._contracts:
|
|
103
|
+
# Check TTL
|
|
104
|
+
created_at = self._creation_times.get(key, 0)
|
|
105
|
+
if now - created_at < self.ttl:
|
|
106
|
+
return self._contracts[key]
|
|
107
|
+
else:
|
|
108
|
+
# Expired, clean up
|
|
109
|
+
del self._contracts[key]
|
|
110
|
+
del self._creation_times[key]
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def _make_key(self, contract_cls: Type, address: str, chain_name: str) -> str:
|
|
114
|
+
"""Create a unique cache key."""
|
|
115
|
+
return f"{contract_cls.__name__}:{chain_name.lower()}:{address.lower()}"
|
|
116
|
+
|
|
117
|
+
def clear(self) -> None:
|
|
118
|
+
"""Clear all cached contracts."""
|
|
119
|
+
with self._lock:
|
|
120
|
+
self._contracts.clear()
|
|
121
|
+
self._creation_times.clear()
|
|
122
|
+
logger.debug("Contract cache cleared")
|
|
123
|
+
|
|
124
|
+
def invalidate(self, contract_cls: Type, address: str, chain_name: str) -> None:
|
|
125
|
+
"""Invalidate a specific contract in the cache."""
|
|
126
|
+
key = self._make_key(contract_cls, address, chain_name)
|
|
127
|
+
with self._lock:
|
|
128
|
+
if key in self._contracts:
|
|
129
|
+
del self._contracts[key]
|
|
130
|
+
del self._creation_times[key]
|
|
131
|
+
logger.debug(f"Invalidated cache for {key}")
|
|
@@ -11,6 +11,7 @@ from web3.contract import Contract
|
|
|
11
11
|
from web3.exceptions import ContractCustomError
|
|
12
12
|
|
|
13
13
|
from iwa.core.chain import ChainInterfaces
|
|
14
|
+
from iwa.core.rpc_monitor import RPCMonitor
|
|
14
15
|
from iwa.core.utils import configure_logger
|
|
15
16
|
|
|
16
17
|
logger = configure_logger()
|
|
@@ -221,6 +222,8 @@ class ContractInstance:
|
|
|
221
222
|
# Re-evaluate self.contract on each retry to get current provider
|
|
222
223
|
# This is critical for RPC rotation to work correctly
|
|
223
224
|
method = getattr(self.contract.functions, method_name)
|
|
225
|
+
# Count the RPC call
|
|
226
|
+
RPCMonitor().increment(f"{self.name}.{method_name}")
|
|
224
227
|
return method(*args).call()
|
|
225
228
|
|
|
226
229
|
return self.chain_interface.with_retry(
|
|
@@ -277,6 +280,10 @@ class ContractInstance:
|
|
|
277
280
|
|
|
278
281
|
try:
|
|
279
282
|
tx_params = self.chain_interface.calculate_transaction_params(built_method, tx_params)
|
|
283
|
+
|
|
284
|
+
# Count the estimateGas/buildTransaction RPC calls
|
|
285
|
+
RPCMonitor().increment(f"{self.name}.{method_name}.estimate_gas")
|
|
286
|
+
|
|
280
287
|
transaction = built_method.build_transaction(tx_params)
|
|
281
288
|
return transaction
|
|
282
289
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""RPC Monitor for tracking API usage."""
|
|
2
|
+
import threading
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from iwa.core.utils import configure_logger
|
|
7
|
+
|
|
8
|
+
logger = configure_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RPCMonitor:
|
|
12
|
+
"""Singleton monitor for tracking RPC usage."""
|
|
13
|
+
|
|
14
|
+
_instance = None
|
|
15
|
+
_lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def __new__(cls):
|
|
18
|
+
"""Create singleton instance."""
|
|
19
|
+
if cls._instance is None:
|
|
20
|
+
with cls._lock:
|
|
21
|
+
if cls._instance is None:
|
|
22
|
+
cls._instance = super(RPCMonitor, cls).__new__(cls)
|
|
23
|
+
cls._instance._initialized = False
|
|
24
|
+
return cls._instance
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize monitor."""
|
|
28
|
+
if self._initialized:
|
|
29
|
+
return
|
|
30
|
+
self._counts: Dict[str, int] = defaultdict(int)
|
|
31
|
+
self._lock = threading.Lock()
|
|
32
|
+
self._initialized = True
|
|
33
|
+
|
|
34
|
+
def increment(self, metric_name: str, count: int = 1):
|
|
35
|
+
"""Increment a metric counter."""
|
|
36
|
+
with self._lock:
|
|
37
|
+
self._counts[metric_name] += count
|
|
38
|
+
|
|
39
|
+
def get_counts(self) -> Dict[str, int]:
|
|
40
|
+
"""Get a copy of current counts."""
|
|
41
|
+
with self._lock:
|
|
42
|
+
return dict(self._counts)
|
|
43
|
+
|
|
44
|
+
def log_stats(self):
|
|
45
|
+
"""Log current statistics."""
|
|
46
|
+
stats = self.get_counts()
|
|
47
|
+
if not stats:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
logger.info("RPC Stats Summary:")
|
|
51
|
+
total = 0
|
|
52
|
+
for k, v in sorted(stats.items()):
|
|
53
|
+
logger.info(f" {k}: {v}")
|
|
54
|
+
total += v
|
|
55
|
+
logger.info(f" TOTAL: {total}")
|
|
56
|
+
|
|
57
|
+
def clear(self):
|
|
58
|
+
"""Clear all counters."""
|
|
59
|
+
with self._lock:
|
|
60
|
+
self._counts.clear()
|
|
@@ -8,7 +8,7 @@ The liveness check (isRatioPass) verifies that the service is making enough mech
|
|
|
8
8
|
requests relative to the time elapsed since the last checkpoint.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Tuple
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
12
|
|
|
13
13
|
from iwa.core.constants import DEFAULT_MECH_CONTRACT_ADDRESS
|
|
14
14
|
from iwa.core.types import EthereumAddress
|
|
@@ -43,27 +43,11 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
43
43
|
"""
|
|
44
44
|
super().__init__(address, chain_name=chain_name)
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.mech_marketplace = None
|
|
52
|
-
|
|
53
|
-
# Get the mech address this checker tracks (legacy or priority mech)
|
|
54
|
-
try:
|
|
55
|
-
agent_mech_function = getattr(self.contract.functions, "agentMech", None)
|
|
56
|
-
self.agent_mech = (
|
|
57
|
-
agent_mech_function().call() if agent_mech_function else DEFAULT_MECH_CONTRACT_ADDRESS
|
|
58
|
-
)
|
|
59
|
-
except Exception:
|
|
60
|
-
self.agent_mech = DEFAULT_MECH_CONTRACT_ADDRESS
|
|
61
|
-
|
|
62
|
-
# Get liveness ratio (requests per second * 1e18)
|
|
63
|
-
try:
|
|
64
|
-
self.liveness_ratio = self.contract.functions.livenessRatio().call()
|
|
65
|
-
except Exception:
|
|
66
|
-
self.liveness_ratio = 0
|
|
46
|
+
# Cache for lazy loading
|
|
47
|
+
self._mech_marketplace: Optional[EthereumAddress] = None
|
|
48
|
+
self._agent_mech: Optional[EthereumAddress] = None
|
|
49
|
+
self._liveness_ratio: Optional[int] = None
|
|
50
|
+
|
|
67
51
|
|
|
68
52
|
def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
|
|
69
53
|
"""Get the nonces for a multisig address.
|
|
@@ -80,6 +64,41 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
80
64
|
nonces = self.contract.functions.getMultisigNonces(multisig).call()
|
|
81
65
|
return (nonces[0], nonces[1])
|
|
82
66
|
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def mech_marketplace(self) -> Optional[EthereumAddress]:
|
|
70
|
+
"""Get the mech marketplace address."""
|
|
71
|
+
if self._mech_marketplace is None:
|
|
72
|
+
try:
|
|
73
|
+
mech_mp_function = getattr(self.contract.functions, "mechMarketplace", None)
|
|
74
|
+
self._mech_marketplace = mech_mp_function().call() if mech_mp_function else None
|
|
75
|
+
except Exception:
|
|
76
|
+
self._mech_marketplace = None
|
|
77
|
+
return self._mech_marketplace
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def agent_mech(self) -> EthereumAddress:
|
|
81
|
+
"""Get the agent mech address."""
|
|
82
|
+
if self._agent_mech is None:
|
|
83
|
+
try:
|
|
84
|
+
agent_mech_function = getattr(self.contract.functions, "agentMech", None)
|
|
85
|
+
self._agent_mech = (
|
|
86
|
+
agent_mech_function().call() if agent_mech_function else DEFAULT_MECH_CONTRACT_ADDRESS
|
|
87
|
+
)
|
|
88
|
+
except Exception:
|
|
89
|
+
self._agent_mech = DEFAULT_MECH_CONTRACT_ADDRESS
|
|
90
|
+
return self._agent_mech
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def liveness_ratio(self) -> int:
|
|
94
|
+
"""Get the liveness ratio."""
|
|
95
|
+
if self._liveness_ratio is None:
|
|
96
|
+
try:
|
|
97
|
+
self._liveness_ratio = self.contract.functions.livenessRatio().call()
|
|
98
|
+
except Exception:
|
|
99
|
+
self._liveness_ratio = 0
|
|
100
|
+
return self._liveness_ratio
|
|
101
|
+
|
|
83
102
|
def is_ratio_pass(
|
|
84
103
|
self,
|
|
85
104
|
current_nonces: Tuple[int, int],
|
|
@@ -101,6 +120,25 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
101
120
|
True if liveness requirements are met.
|
|
102
121
|
|
|
103
122
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
# Optimized implementation to avoid RPC call
|
|
124
|
+
current_safe, current_requests = current_nonces
|
|
125
|
+
last_safe, last_requests = last_nonces
|
|
126
|
+
|
|
127
|
+
diff_safe = current_safe - last_safe
|
|
128
|
+
diff_requests = current_requests - last_requests
|
|
129
|
+
|
|
130
|
+
# 1. Check if requests exceed transactions (impossible in valid operation)
|
|
131
|
+
# Also check for negative diffs (data corruption/stale data edge case)
|
|
132
|
+
if diff_requests > diff_safe or diff_requests < 0 or diff_safe < 0:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# 2. Check time difference validity
|
|
136
|
+
if ts_diff == 0:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# 3. Check ratio
|
|
140
|
+
# ratio = (diffRequests * 1e18) / ts_diff >= livenessRatio
|
|
141
|
+
# We use integer arithmetic as per Solidity
|
|
142
|
+
ratio = (diff_requests * 10**18) // ts_diff
|
|
143
|
+
|
|
144
|
+
return ratio >= self.liveness_ratio
|
|
@@ -79,22 +79,9 @@ class StakingContract(ContractInstance):
|
|
|
79
79
|
self.chain_name = chain_name
|
|
80
80
|
self._contract_params_cache: Dict[str, int] = {}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
activity_checker_address, chain_name=chain_name
|
|
86
|
-
)
|
|
87
|
-
self.activity_checker_address = activity_checker_address
|
|
88
|
-
|
|
89
|
-
# Cache contract parameters
|
|
90
|
-
self.available_rewards = self.call("availableRewards")
|
|
91
|
-
self.balance = self.call("balance")
|
|
92
|
-
self.liveness_period = self.call("livenessPeriod")
|
|
93
|
-
self.rewards_per_second = self.call("rewardsPerSecond")
|
|
94
|
-
self.max_num_services = self.call("maxNumServices")
|
|
95
|
-
self.min_staking_deposit = self.call("minStakingDeposit")
|
|
96
|
-
self.min_staking_duration_hours = self.call("minStakingDuration") / 3600
|
|
97
|
-
self.staking_token_address = self.call("stakingToken")
|
|
82
|
+
self._activity_checker: Optional[ActivityCheckerContract] = None
|
|
83
|
+
self._activity_checker_address: Optional[EthereumAddress] = None
|
|
84
|
+
|
|
98
85
|
|
|
99
86
|
def get_requirements(self) -> Dict[str, Union[str, int]]:
|
|
100
87
|
"""Get the contract requirements for token and deposits.
|
|
@@ -193,10 +180,16 @@ class StakingContract(ContractInstance):
|
|
|
193
180
|
remaining_seconds = (epoch_end - datetime.now(timezone.utc)).total_seconds()
|
|
194
181
|
|
|
195
182
|
# Check liveness ratio using activity checker
|
|
183
|
+
# logic: use the latest of (service_start_time, global_checkpoint_time)
|
|
184
|
+
# If service started AFTER global checkpoint, use service_start_time.
|
|
185
|
+
# If service was already running, use global_checkpoint_time.
|
|
186
|
+
global_ts_checkpoint = self.ts_checkpoint()
|
|
187
|
+
effective_ts_start = max(ts_start, global_ts_checkpoint)
|
|
188
|
+
|
|
196
189
|
liveness_passed = self.is_liveness_ratio_passed(
|
|
197
190
|
current_nonces=current_nonces,
|
|
198
191
|
last_nonces=(last_safe_nonce, last_mech_requests),
|
|
199
|
-
ts_start=
|
|
192
|
+
ts_start=effective_ts_start,
|
|
200
193
|
)
|
|
201
194
|
|
|
202
195
|
return {
|
|
@@ -223,8 +216,36 @@ class StakingContract(ContractInstance):
|
|
|
223
216
|
return StakingState(self.call("getStakingState", service_id))
|
|
224
217
|
|
|
225
218
|
def ts_checkpoint(self) -> int:
|
|
226
|
-
"""Get the timestamp of the last checkpoint.
|
|
227
|
-
|
|
219
|
+
"""Get the timestamp of the last checkpoint.
|
|
220
|
+
|
|
221
|
+
Cached until the estimated end of the current epoch (ts_checkpoint + liveness_period).
|
|
222
|
+
"""
|
|
223
|
+
now = time.time()
|
|
224
|
+
cache_key = "ts_checkpoint"
|
|
225
|
+
|
|
226
|
+
# Check if we have a valid cached value
|
|
227
|
+
if cache_key in self._contract_params_cache:
|
|
228
|
+
ts = self._contract_params_cache[cache_key]
|
|
229
|
+
# Use liveness period to determine if we should re-check
|
|
230
|
+
if now < ts + self.liveness_period:
|
|
231
|
+
return ts
|
|
232
|
+
|
|
233
|
+
# If past expected epoch end, check at most once per minute
|
|
234
|
+
last_checked = self._contract_params_cache.get(f"{cache_key}_last_checked", 0)
|
|
235
|
+
if now - last_checked < 60:
|
|
236
|
+
return ts
|
|
237
|
+
|
|
238
|
+
# Fetch new value
|
|
239
|
+
ts = self.call("tsCheckpoint")
|
|
240
|
+
self._contract_params_cache[cache_key] = ts
|
|
241
|
+
self._contract_params_cache[f"{cache_key}_last_checked"] = now
|
|
242
|
+
return ts
|
|
243
|
+
|
|
244
|
+
def clear_epoch_cache(self) -> None:
|
|
245
|
+
"""Clear cache for epoch-dependent properties."""
|
|
246
|
+
self._contract_params_cache.pop("ts_checkpoint", None)
|
|
247
|
+
self._contract_params_cache.pop("ts_checkpoint_last_checked", None)
|
|
248
|
+
logger.debug(f"Cleared epoch cache for StakingContract {self.address}")
|
|
228
249
|
|
|
229
250
|
def get_required_requests(self, use_liveness_period: bool = True) -> int:
|
|
230
251
|
"""Calculate the required requests for the current epoch.
|
|
@@ -246,6 +267,81 @@ class StakingContract(ContractInstance):
|
|
|
246
267
|
(time_diff * self.activity_checker.liveness_ratio) / 1e18 + requests_safety_margin
|
|
247
268
|
)
|
|
248
269
|
|
|
270
|
+
@property
|
|
271
|
+
def activity_checker_address_value(self) -> EthereumAddress:
|
|
272
|
+
"""Get the activity checker address."""
|
|
273
|
+
if self._activity_checker_address is None:
|
|
274
|
+
self._activity_checker_address = self.call("activityChecker")
|
|
275
|
+
return self._activity_checker_address
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def activity_checker_address(self) -> EthereumAddress:
|
|
279
|
+
"""Backwards compatibility for activity_checker_address."""
|
|
280
|
+
return self.activity_checker_address_value
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def activity_checker(self) -> ActivityCheckerContract:
|
|
284
|
+
"""Get the activity checker contract."""
|
|
285
|
+
if self._activity_checker is None:
|
|
286
|
+
self._activity_checker = ActivityCheckerContract(
|
|
287
|
+
self.activity_checker_address_value, chain_name=self.chain_name
|
|
288
|
+
)
|
|
289
|
+
return self._activity_checker
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def available_rewards(self) -> int:
|
|
293
|
+
"""Get available rewards."""
|
|
294
|
+
if "availableRewards" not in self._contract_params_cache:
|
|
295
|
+
self._contract_params_cache["availableRewards"] = self.call("availableRewards")
|
|
296
|
+
return self._contract_params_cache["availableRewards"]
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def balance(self) -> int:
|
|
300
|
+
"""Get contract balance."""
|
|
301
|
+
if "balance" not in self._contract_params_cache:
|
|
302
|
+
self._contract_params_cache["balance"] = self.call("balance")
|
|
303
|
+
return self._contract_params_cache["balance"]
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def liveness_period(self) -> int:
|
|
307
|
+
"""Get liveness period."""
|
|
308
|
+
if "livenessPeriod" not in self._contract_params_cache:
|
|
309
|
+
self._contract_params_cache["livenessPeriod"] = self.call("livenessPeriod")
|
|
310
|
+
return self._contract_params_cache["livenessPeriod"]
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def rewards_per_second(self) -> int:
|
|
314
|
+
"""Get rewards per second."""
|
|
315
|
+
if "rewardsPerSecond" not in self._contract_params_cache:
|
|
316
|
+
self._contract_params_cache["rewardsPerSecond"] = self.call("rewardsPerSecond")
|
|
317
|
+
return self._contract_params_cache["rewardsPerSecond"]
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def max_num_services(self) -> int:
|
|
321
|
+
"""Get max number of services."""
|
|
322
|
+
if "maxNumServices" not in self._contract_params_cache:
|
|
323
|
+
self._contract_params_cache["maxNumServices"] = self.call("maxNumServices")
|
|
324
|
+
return self._contract_params_cache["maxNumServices"]
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def min_staking_deposit(self) -> int:
|
|
328
|
+
"""Get min staking deposit."""
|
|
329
|
+
if "minStakingDeposit" not in self._contract_params_cache:
|
|
330
|
+
self._contract_params_cache["minStakingDeposit"] = self.call("minStakingDeposit")
|
|
331
|
+
return self._contract_params_cache["minStakingDeposit"]
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def min_staking_duration_hours(self) -> float:
|
|
335
|
+
"""Get min staking duration in hours."""
|
|
336
|
+
return self.min_staking_duration / 3600
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def staking_token_address(self) -> EthereumAddress:
|
|
340
|
+
"""Get staking token address."""
|
|
341
|
+
if "stakingToken" not in self._contract_params_cache:
|
|
342
|
+
self._contract_params_cache["stakingToken"] = self.call("stakingToken")
|
|
343
|
+
return self._contract_params_cache["stakingToken"]
|
|
344
|
+
|
|
249
345
|
def is_liveness_ratio_passed(
|
|
250
346
|
self,
|
|
251
347
|
current_nonces: tuple,
|