ipor-fusion 4.0.0.dev106__tar.gz → 4.0.0.dev110__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.
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/AGENTS.md +13 -4
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/MIGRATION_4.md +62 -3
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/PKG-INFO +11 -3
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/README.md +10 -2
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/__init__.py +16 -0
- ipor_fusion-4.0.0.dev110/src/ipor_fusion/cache.py +204 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_info_view.py +5 -4
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_markets_view.py +19 -26
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/context.py +15 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/explorer.py +23 -11
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/server.py +4 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_data.py +67 -47
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_info.py +8 -6
- ipor_fusion-4.0.0.dev110/tests/test_cache.py +362 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_commands.py +18 -24
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_helpers.py +43 -97
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_market_morpho_blue.py +1 -8
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_info_golden.py +60 -23
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_predates_getters.py +3 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_explorer.py +38 -18
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_live.py +1 -5
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_public_api.py +7 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/typed_client/consumer.py +12 -0
- ipor_fusion-4.0.0.dev106/src/ipor_fusion/cache.py +0 -86
- ipor_fusion-4.0.0.dev106/tests/test_cache.py +0 -99
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/access.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/contract.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fuse.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/discovery.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/models.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/morpho_api.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/types.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_managers.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_positions.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_reads.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/conftest.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/clean.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/clean.txt +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/rich.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/rich.txt +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_pending_requests_scan.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_contract_call.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_indexer.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_quote.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_error_hierarchy.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fusion_instance.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_readers.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_models.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_server.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_reader_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_simulator_blocks.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/typed_client/rejected.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/uv.lock +0 -0
|
@@ -158,7 +158,7 @@ the lifecycle matrix follows that.
|
|
|
158
158
|
subpackage (`core`, `config`, `fuses`, `readers`, `crosschain`,
|
|
159
159
|
`crosschain.fuses`, `crosschain.ccip`, `crosschain.stargate`) also goes in that
|
|
160
160
|
subpackage's `__all__`. `ipor_fusion.guide` keeps its own namespace. The
|
|
161
|
-
public modules (`about`, `chains`, `dependency_graph`, `errors`, `explorer`,
|
|
161
|
+
public modules (`about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`,
|
|
162
162
|
`field_docs`, `market_ids`, `market_info`, `morpho_api`, `substrates`, `types`,
|
|
163
163
|
`validation`, `vault_data`, `vault_info`) stay importable for the names they
|
|
164
164
|
define; `cli`, `mcp` and every other module are internal. Examples, the guide
|
|
@@ -204,7 +204,13 @@ the lifecycle matrix follows that.
|
|
|
204
204
|
document rendered from the snapshot alone (contract names via an injected
|
|
205
205
|
`resolve_name`),
|
|
206
206
|
and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
|
|
207
|
-
`cache.py` the
|
|
207
|
+
`cache.py` the `ContractCache` protocol (`get`/`put` of immutable facts keyed
|
|
208
|
+
`CacheKey(chain_id, address, field)`), `FileContractCache` (one JSON file, atomic
|
|
209
|
+
replace, corrupt/unwritable degrades to memory) and `NullCache`; every SDK use goes
|
|
210
|
+
through `cache_get`/`cache_put` (a failing cache is a miss, never an error) on
|
|
211
|
+
`ctx.cache` (`Web3Context(cache=...)`, default `default_contract_cache()` under
|
|
212
|
+
`$XDG_CACHE_HOME/ipor-fusion/`) or `get_contract_name(cache=...)`. Never cache an
|
|
213
|
+
absence (no code, no answer): it can change at a later block.
|
|
208
214
|
`morpho_api.py` the Morpho public GraphQL API client (`fetch_market`, `fetch_vault`,
|
|
209
215
|
`PUBLIC_ALLOCATOR_ADDRESSES`); `market_info.py` — `build_morpho_blue_market_json`,
|
|
210
216
|
`build_meta_morpho_json`, the `fusion market` JSON documents.
|
|
@@ -388,8 +394,11 @@ byte, e.g. Ebisu ZAPPER/REGISTRY).
|
|
|
388
394
|
role-accounts, oracle-mapping). `vault info` runs independent fetch pipelines
|
|
389
395
|
(vault/asset, fees, WithdrawManager, markets, roles) concurrently on a
|
|
390
396
|
`ThreadPoolExecutor`, each a short chain of `Multicall3` batches; keep new
|
|
391
|
-
reads inside a batch rather than adding per-call round trips. Contract names
|
|
392
|
-
|
|
397
|
+
reads inside a batch rather than adding per-call round trips. Contract names,
|
|
398
|
+
token symbols and decimals and deployment blocks are cached per chain in
|
|
399
|
+
`$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`<chain>:<address>:<field>`
|
|
400
|
+
keys); tests redirect it with `monkeypatch.setenv("XDG_CACHE_HOME", ...)` or
|
|
401
|
+
inject a cache.
|
|
393
402
|
|
|
394
403
|
Every CLI command has an MCP tool (`fusion changelog` maps to `server_info`;
|
|
395
404
|
the rest share names). Read-only: `vault_info`, `vault_validate`,
|
|
@@ -334,6 +334,66 @@ argument (`chain_id=`) for another chain. An external signer still needs the rea
|
|
|
334
334
|
target address (`call.to`) and builds the chain-qualified transaction itself, or
|
|
335
335
|
gets it from `call.build_transaction(ctx)`.
|
|
336
336
|
|
|
337
|
+
## Contract cache
|
|
338
|
+
|
|
339
|
+
Contract names, token symbols and decimals and deployment blocks are cached in a
|
|
340
|
+
`ContractCache`, injected instead of always written to `~/.cache`:
|
|
341
|
+
|
|
342
|
+
- `Web3Context(..., cache=...)` and `Web3Context.from_url(..., cache=...)` take one
|
|
343
|
+
(keyword-only); `ctx.cache` returns it, and `ctx.at_block(...)` keeps it. The
|
|
344
|
+
default (`None`) is `default_contract_cache()`, a `FileContractCache` at
|
|
345
|
+
`$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`~/.cache/...` when unset),
|
|
346
|
+
resolved when used rather than at import. `NullCache()` keeps nothing;
|
|
347
|
+
`FileContractCache(path)` moves the file; any object with `get(keys)` and
|
|
348
|
+
`put(entries)` over `CacheKey(chain_id, address, field)` → `str` fits the
|
|
349
|
+
protocol (for example a shared store in a service).
|
|
350
|
+
- `resolve_token_symbol`, `resolve_token_decimals`, the snapshot's token metadata
|
|
351
|
+
(`fetch_vault_data(metadata=True)`) and `fetch_deployment_info` use `ctx.cache`.
|
|
352
|
+
`get_contract_name(chain_id, address, api_key=None, *, cache=None)` takes the cache
|
|
353
|
+
explicitly (default: the file cache); pass `cache=ctx.cache` to share the
|
|
354
|
+
context's policy. The CLI's text and JSON output name contracts through the
|
|
355
|
+
context's cache.
|
|
356
|
+
- `build_vault_info_json(data)` without `resolve_name` names no contract (every
|
|
357
|
+
`contract` is `"?"`) instead of reading names from the on-disk cache; pass
|
|
358
|
+
`resolve_name=partial(get_contract_name, chain_id, api_key=..., cache=ctx.cache)`
|
|
359
|
+
for the CLI's document.
|
|
360
|
+
- `ipor_fusion.cache` is a public module now and exports `ContractCache`,
|
|
361
|
+
`FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
|
|
362
|
+
`default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
|
|
363
|
+
are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
|
|
364
|
+
`load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
|
|
365
|
+
`DEPLOYMENT_CACHE_FILE`. `cache_key(chain_id, address, field)` returns a `CacheKey`
|
|
366
|
+
and raises `ValueError` for an address that is not 20 bytes of hex, so
|
|
367
|
+
`get_contract_name` does too.
|
|
368
|
+
|
|
369
|
+
**File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
|
|
370
|
+
`symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
|
|
371
|
+
load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
|
|
372
|
+
3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
|
|
373
|
+
discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
|
|
374
|
+
is no longer read: a vault's deployment block is resolved again (it needs an
|
|
375
|
+
Etherscan key) and the old file can be deleted.
|
|
376
|
+
|
|
377
|
+
**Behavior.**
|
|
378
|
+
|
|
379
|
+
- The same address on two chains has two entries: a token's symbol or decimals read
|
|
380
|
+
on one chain is never served for another.
|
|
381
|
+
- Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
|
|
382
|
+
address without code, but no longer stores it, so a token deployed later (or seen at
|
|
383
|
+
a later block) resolves.
|
|
384
|
+
- A write merges with the file as it is on disk and replaces it atomically (a
|
|
385
|
+
temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
|
|
386
|
+
processes writing at once never leave a torn file; one of two concurrent writers may
|
|
387
|
+
lose its newest entry, which is read again later.
|
|
388
|
+
- A cache never fails a read: a corrupt or unreadable file reads as empty (logged
|
|
389
|
+
warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
|
|
390
|
+
directory (read-only file system) keeps the entries in memory for the process; a
|
|
391
|
+
custom cache that raises is treated as a miss, and so is a stored number that does
|
|
392
|
+
not parse.
|
|
393
|
+
- `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
|
|
394
|
+
transaction or its block cannot be read; a cache failure is no longer reported as
|
|
395
|
+
one (the deployment block is returned).
|
|
396
|
+
|
|
337
397
|
## Behavior changes
|
|
338
398
|
|
|
339
399
|
- Two fuses are equal (and hash alike) only when they have the same class and the
|
|
@@ -371,9 +431,8 @@ gets it from `call.build_transaction(ctx)`.
|
|
|
371
431
|
- `TransactionError.tx_hash` is `0x`-prefixed.
|
|
372
432
|
- Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
|
|
373
433
|
a value that is not an address.
|
|
374
|
-
- The
|
|
375
|
-
|
|
376
|
-
for the other.
|
|
434
|
+
- The contract cache keys every entry by chain and lowercase address and no longer
|
|
435
|
+
caches an absence (see [Contract cache](#contract-cache)).
|
|
377
436
|
|
|
378
437
|
## Errors
|
|
379
438
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ipor-fusion
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev110
|
|
4
4
|
Summary: The IPOR Fusion SDK for Python
|
|
5
5
|
Project-URL: homepage, https://ipor.io
|
|
6
6
|
Project-URL: repository, https://github.com/IPOR-Labs/ipor-fusion.py
|
|
@@ -381,11 +381,11 @@ release; adding one does not.
|
|
|
381
381
|
`crosschain.fuses`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
|
|
382
382
|
(the packaged docs) keeps its names in its own namespace.
|
|
383
383
|
- **Public modules** stay importable for the top-level names they define:
|
|
384
|
-
`about`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
|
|
384
|
+
`about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
|
|
385
385
|
`market_ids`, `market_info`, `morpho_api`, `substrates`, `types`, `validation`,
|
|
386
386
|
`vault_data` and `vault_info` (all under `ipor_fusion.`).
|
|
387
387
|
- **Internal:** `ipor_fusion.cli`, `ipor_fusion.mcp`, every other module (e.g.
|
|
388
|
-
`
|
|
388
|
+
`vault_reads`, the submodules of the public subpackages), underscore
|
|
389
389
|
names, and names in no `__all__`. They change without notice; the CLI and the
|
|
390
390
|
MCP server are stable as commands and tools, not as Python modules.
|
|
391
391
|
- **Preview features** (`PREVIEW_FEATURES` in `ipor_fusion.about`: crosschain,
|
|
@@ -495,6 +495,14 @@ fields as unavailable (`null`) rather than partial. A `LogSource` on the context
|
|
|
495
495
|
a service that caches an explorer's log API, answers those scans instead
|
|
496
496
|
(`Web3Context(log_source=…)`); when it raises `LogSourceUnavailable` the RPC scan runs.
|
|
497
497
|
|
|
498
|
+
Immutable contract facts (token symbols and decimals, contract names, deployment blocks)
|
|
499
|
+
are cached per chain and address in the context's `ContractCache`. The default,
|
|
500
|
+
`FileContractCache`, is `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json`
|
|
501
|
+
(`~/.cache/...`), written atomically and ignored when corrupt or unwritable. A service
|
|
502
|
+
passes `Web3Context(..., cache=NullCache())` to keep nothing, `FileContractCache(path)`
|
|
503
|
+
to move it, or its own `get` / `put` implementation; `get_contract_name(..., cache=)`
|
|
504
|
+
takes the same object.
|
|
505
|
+
|
|
498
506
|
## Development
|
|
499
507
|
|
|
500
508
|
```bash
|
|
@@ -333,11 +333,11 @@ release; adding one does not.
|
|
|
333
333
|
`crosschain.fuses`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
|
|
334
334
|
(the packaged docs) keeps its names in its own namespace.
|
|
335
335
|
- **Public modules** stay importable for the top-level names they define:
|
|
336
|
-
`about`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
|
|
336
|
+
`about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
|
|
337
337
|
`market_ids`, `market_info`, `morpho_api`, `substrates`, `types`, `validation`,
|
|
338
338
|
`vault_data` and `vault_info` (all under `ipor_fusion.`).
|
|
339
339
|
- **Internal:** `ipor_fusion.cli`, `ipor_fusion.mcp`, every other module (e.g.
|
|
340
|
-
`
|
|
340
|
+
`vault_reads`, the submodules of the public subpackages), underscore
|
|
341
341
|
names, and names in no `__all__`. They change without notice; the CLI and the
|
|
342
342
|
MCP server are stable as commands and tools, not as Python modules.
|
|
343
343
|
- **Preview features** (`PREVIEW_FEATURES` in `ipor_fusion.about`: crosschain,
|
|
@@ -447,6 +447,14 @@ fields as unavailable (`null`) rather than partial. A `LogSource` on the context
|
|
|
447
447
|
a service that caches an explorer's log API, answers those scans instead
|
|
448
448
|
(`Web3Context(log_source=…)`); when it raises `LogSourceUnavailable` the RPC scan runs.
|
|
449
449
|
|
|
450
|
+
Immutable contract facts (token symbols and decimals, contract names, deployment blocks)
|
|
451
|
+
are cached per chain and address in the context's `ContractCache`. The default,
|
|
452
|
+
`FileContractCache`, is `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json`
|
|
453
|
+
(`~/.cache/...`), written atomically and ignored when corrupt or unwritable. A service
|
|
454
|
+
passes `Web3Context(..., cache=NullCache())` to keep nothing, `FileContractCache(path)`
|
|
455
|
+
to move it, or its own `get` / `put` implementation; `get_contract_name(..., cache=)`
|
|
456
|
+
takes the same object.
|
|
457
|
+
|
|
450
458
|
## Development
|
|
451
459
|
|
|
452
460
|
```bash
|
|
@@ -7,6 +7,15 @@ from ipor_fusion.about import (
|
|
|
7
7
|
read_changelog,
|
|
8
8
|
repository_url,
|
|
9
9
|
)
|
|
10
|
+
from ipor_fusion.cache import (
|
|
11
|
+
CacheField,
|
|
12
|
+
CacheKey,
|
|
13
|
+
ContractCache,
|
|
14
|
+
FileContractCache,
|
|
15
|
+
NullCache,
|
|
16
|
+
cache_key,
|
|
17
|
+
default_contract_cache,
|
|
18
|
+
)
|
|
10
19
|
from ipor_fusion.chains import (
|
|
11
20
|
CHAIN_NAME_TO_ID,
|
|
12
21
|
CHAIN_NAMES,
|
|
@@ -741,6 +750,13 @@ __all__ = [
|
|
|
741
750
|
"decode_custom_error",
|
|
742
751
|
"get_revert_reason",
|
|
743
752
|
"register_custom_errors",
|
|
753
|
+
"CacheField",
|
|
754
|
+
"CacheKey",
|
|
755
|
+
"ContractCache",
|
|
756
|
+
"FileContractCache",
|
|
757
|
+
"NullCache",
|
|
758
|
+
"cache_key",
|
|
759
|
+
"default_contract_cache",
|
|
744
760
|
"get_contract_name",
|
|
745
761
|
"get_deployment_tx",
|
|
746
762
|
"CHAIN_NAMES",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Caches of immutable per-contract chain facts: contract names, token symbols
|
|
2
|
+
and decimals, deployment blocks. A `ContractCache` is injected through
|
|
3
|
+
`Web3Context(cache=...)` (and `get_contract_name(cache=...)`); the default is
|
|
4
|
+
`FileContractCache` at ``$XDG_CACHE_HOME/ipor-fusion/contract_cache.json``,
|
|
5
|
+
and `NullCache` disables caching.
|
|
6
|
+
|
|
7
|
+
A cache is an optimization, never a source of failure: the SDK reads and
|
|
8
|
+
writes it through `cache_get` / `cache_put`, which log and swallow any error a
|
|
9
|
+
cache raises, and `FileContractCache` itself degrades to an in-memory map when
|
|
10
|
+
its file is corrupt, unreadable or unwritable."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
import threading
|
|
19
|
+
from collections.abc import Iterable, Mapping
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Literal, NamedTuple, Protocol, cast, get_args
|
|
22
|
+
|
|
23
|
+
from eth_typing import HexAddress
|
|
24
|
+
|
|
25
|
+
from ipor_fusion.types import ChainId, parse_hex_address
|
|
26
|
+
|
|
27
|
+
_logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
CacheField = Literal[
|
|
30
|
+
"name", "symbol", "decimals", "deployment_block", "deployment_timestamp"
|
|
31
|
+
]
|
|
32
|
+
_FIELDS: frozenset[str] = frozenset(get_args(CacheField))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CacheKey(NamedTuple):
|
|
36
|
+
"""One fact about the contract at ``address`` on ``chain_id``. Build it
|
|
37
|
+
with `cache_key`, which normalizes the address to lowercase."""
|
|
38
|
+
|
|
39
|
+
chain_id: ChainId
|
|
40
|
+
address: HexAddress
|
|
41
|
+
field: CacheField
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cache_key(chain_id: ChainId, address: HexAddress, field: CacheField) -> CacheKey:
|
|
45
|
+
return CacheKey(chain_id, parse_hex_address(address), field)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ContractCache(Protocol):
|
|
49
|
+
"""Immutable facts keyed by chain, address and field, as strings. Store
|
|
50
|
+
only what cannot change for that key; absence (no code, no answer) is
|
|
51
|
+
never stored."""
|
|
52
|
+
|
|
53
|
+
def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
|
|
54
|
+
"""The cached values of ``keys``; a miss is absent from the result."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def put(self, entries: Mapping[CacheKey, str]) -> None: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NullCache:
|
|
61
|
+
"""A `ContractCache` that stores nothing: every read goes to the chain or
|
|
62
|
+
the explorer."""
|
|
63
|
+
|
|
64
|
+
def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
def put(self, entries: Mapping[CacheKey, str]) -> None:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _encode(key: CacheKey) -> str:
|
|
72
|
+
return f"{key.chain_id}:{key.address}:{key.field}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _decode(raw: str) -> CacheKey | None:
|
|
76
|
+
"""The key of one stored entry, ``None`` for an entry that cannot be
|
|
77
|
+
attributed to a chain, an address and a field. A ``<chain>:<address>`` entry is a contract
|
|
78
|
+
name (the 3.x layout); 3.x token entries (``symbol:<address>``) carry no
|
|
79
|
+
chain and are dropped."""
|
|
80
|
+
parts = raw.split(":")
|
|
81
|
+
if len(parts) == 2:
|
|
82
|
+
parts.append("name")
|
|
83
|
+
if len(parts) != 3 or not parts[0].isdigit() or parts[2] not in _FIELDS:
|
|
84
|
+
return None
|
|
85
|
+
chain, address, field = parts
|
|
86
|
+
try:
|
|
87
|
+
return cache_key(
|
|
88
|
+
ChainId(int(chain)), parse_hex_address(address), cast(CacheField, field)
|
|
89
|
+
)
|
|
90
|
+
except ValueError:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FileContractCache:
|
|
95
|
+
"""A `ContractCache` in one JSON file, loaded once per instance.
|
|
96
|
+
|
|
97
|
+
A write merges into the file as it is on disk (another process may have
|
|
98
|
+
added entries) and replaces it atomically (a temporary file in the same
|
|
99
|
+
directory, then `os.replace`), so a crash or a concurrent writer never
|
|
100
|
+
leaves a torn file; concurrent writers may drop each other's latest entry,
|
|
101
|
+
which is refetched later. A corrupt or unreadable file reads as empty and
|
|
102
|
+
is rewritten by the next write; a failed write is logged and the entry
|
|
103
|
+
kept in memory only."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, path: Path) -> None:
|
|
106
|
+
self.path = path
|
|
107
|
+
self._lock = threading.Lock()
|
|
108
|
+
self._entries: dict[CacheKey, str] | None = None
|
|
109
|
+
|
|
110
|
+
def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
|
|
111
|
+
with self._lock:
|
|
112
|
+
entries = self._loaded()
|
|
113
|
+
return {key: entries[key] for key in keys if key in entries}
|
|
114
|
+
|
|
115
|
+
def put(self, entries: Mapping[CacheKey, str]) -> None:
|
|
116
|
+
if not entries:
|
|
117
|
+
return
|
|
118
|
+
with self._lock:
|
|
119
|
+
merged = self._read() | (self._entries or {}) | dict(entries)
|
|
120
|
+
self._entries = merged
|
|
121
|
+
try:
|
|
122
|
+
self._write(merged)
|
|
123
|
+
except OSError as exc:
|
|
124
|
+
_logger.warning("contract cache %s not written: %s", self.path, exc)
|
|
125
|
+
|
|
126
|
+
def _loaded(self) -> dict[CacheKey, str]:
|
|
127
|
+
if self._entries is None:
|
|
128
|
+
self._entries = self._read()
|
|
129
|
+
return self._entries
|
|
130
|
+
|
|
131
|
+
def _read(self) -> dict[CacheKey, str]:
|
|
132
|
+
try:
|
|
133
|
+
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
|
134
|
+
except FileNotFoundError:
|
|
135
|
+
return {}
|
|
136
|
+
except (OSError, ValueError) as exc:
|
|
137
|
+
_logger.warning("contract cache %s unreadable, ignored: %s", self.path, exc)
|
|
138
|
+
return {}
|
|
139
|
+
if not isinstance(raw, dict):
|
|
140
|
+
_logger.warning(
|
|
141
|
+
"contract cache %s is not a JSON object, ignored", self.path
|
|
142
|
+
)
|
|
143
|
+
return {}
|
|
144
|
+
entries: dict[CacheKey, str] = {}
|
|
145
|
+
for raw_key, value in raw.items():
|
|
146
|
+
key = _decode(raw_key)
|
|
147
|
+
if key is not None and isinstance(value, str):
|
|
148
|
+
# A canonical entry wins over a 3.x one spelled otherwise.
|
|
149
|
+
if key not in entries or _encode(key) == raw_key:
|
|
150
|
+
entries[key] = value
|
|
151
|
+
return entries
|
|
152
|
+
|
|
153
|
+
def _write(self, entries: dict[CacheKey, str]) -> None:
|
|
154
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
payload = json.dumps(
|
|
156
|
+
{_encode(key): entries[key] for key in sorted(entries)}, indent=2
|
|
157
|
+
)
|
|
158
|
+
fd, tmp = tempfile.mkstemp(
|
|
159
|
+
dir=self.path.parent, prefix=f".{self.path.name}.", suffix=".tmp"
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
163
|
+
fh.write(payload)
|
|
164
|
+
os.replace(tmp, self.path)
|
|
165
|
+
except BaseException:
|
|
166
|
+
Path(tmp).unlink(missing_ok=True)
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def default_cache_path() -> Path:
|
|
171
|
+
"""``$XDG_CACHE_HOME/ipor-fusion/contract_cache.json``
|
|
172
|
+
(``~/.cache/...`` when unset), resolved at call time."""
|
|
173
|
+
home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
174
|
+
return home / "ipor-fusion" / "contract_cache.json"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_defaults: dict[Path, FileContractCache] = {}
|
|
178
|
+
_defaults_lock = threading.Lock()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def default_contract_cache() -> FileContractCache:
|
|
182
|
+
"""The process-wide `FileContractCache` at `default_cache_path`."""
|
|
183
|
+
path = default_cache_path()
|
|
184
|
+
with _defaults_lock:
|
|
185
|
+
if (cache := _defaults.get(path)) is None:
|
|
186
|
+
cache = _defaults[path] = FileContractCache(path)
|
|
187
|
+
return cache
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cache_get(cache: ContractCache, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
|
|
191
|
+
"""``cache.get(keys)``; a cache that raises reads as all misses."""
|
|
192
|
+
try:
|
|
193
|
+
return cache.get(keys)
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
_logger.warning("contract cache read failed, ignored: %s", exc)
|
|
196
|
+
return {}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cache_put(cache: ContractCache, entries: Mapping[CacheKey, str]) -> None:
|
|
200
|
+
"""``cache.put(entries)``; a cache that raises loses the entries."""
|
|
201
|
+
try:
|
|
202
|
+
cache.put(entries)
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
_logger.warning("contract cache write failed, ignored: %s", exc)
|
{ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_info_view.py
RENAMED
|
@@ -85,10 +85,11 @@ def _print_vault_info(
|
|
|
85
85
|
data.deployment_error,
|
|
86
86
|
) = fetch_deployment_info(ctx, data.vault, api_key=api_key)
|
|
87
87
|
|
|
88
|
+
resolve_name = partial(
|
|
89
|
+
get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
|
|
90
|
+
)
|
|
88
91
|
if json_output:
|
|
89
|
-
result = build_vault_info_json(
|
|
90
|
-
data, resolve_name=partial(get_contract_name, chain_id, api_key=api_key)
|
|
91
|
-
)
|
|
92
|
+
result = build_vault_info_json(data, resolve_name=resolve_name)
|
|
92
93
|
click.echo(json.dumps(result, indent=2))
|
|
93
94
|
return
|
|
94
95
|
|
|
@@ -102,7 +103,7 @@ def _print_vault_info(
|
|
|
102
103
|
|
|
103
104
|
_print_role_accounts(_role_accounts_json(data))
|
|
104
105
|
|
|
105
|
-
_print_markets(data,
|
|
106
|
+
_print_markets(data, resolve_name)
|
|
106
107
|
|
|
107
108
|
validation = validate_vault_state(data)
|
|
108
109
|
click.echo("ERC20 Balances (vault holdings):")
|
{ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_markets_view.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Sequence
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
4
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
@@ -13,12 +13,11 @@ from ipor_fusion.cli.vault_rendering import (
|
|
|
13
13
|
_print_table,
|
|
14
14
|
_substrate_details,
|
|
15
15
|
)
|
|
16
|
-
from ipor_fusion.explorer import get_contract_name
|
|
17
16
|
from ipor_fusion.substrates import (
|
|
18
17
|
decode_substrate,
|
|
19
18
|
format_market_label,
|
|
20
19
|
)
|
|
21
|
-
from ipor_fusion.types import
|
|
20
|
+
from ipor_fusion.types import FuseAddress, MarketId, parse_hex_address
|
|
22
21
|
from ipor_fusion.vault_data import (
|
|
23
22
|
GET_MARKET_SUBSTRATES,
|
|
24
23
|
VaultData,
|
|
@@ -27,6 +26,10 @@ from ipor_fusion.vault_info import (
|
|
|
27
26
|
_CAPABILITY_BALANCE_FUSES,
|
|
28
27
|
)
|
|
29
28
|
|
|
29
|
+
# Address to contract name, "" when unknown (`get_contract_name` bound to the
|
|
30
|
+
# chain, API key and the context's cache).
|
|
31
|
+
NameResolver = Callable[[HexAddress], str]
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
def _print_lending_health(data: VaultData) -> None: # noqa: C901
|
|
32
35
|
lh = data.lending_health
|
|
@@ -154,16 +157,14 @@ def _print_health_lines(market: Any, indent: str) -> None:
|
|
|
154
157
|
click.secho(f"{indent}Status: {status}", fg=color, bold=bold)
|
|
155
158
|
|
|
156
159
|
|
|
157
|
-
def _print_markets(data: VaultData,
|
|
160
|
+
def _print_markets(data: VaultData, resolve_name: NameResolver) -> None:
|
|
158
161
|
"""Fuses, balance fuses, dependency graph and substrates."""
|
|
159
|
-
chain_id = data.chain_id
|
|
160
162
|
_print_fuse_section(
|
|
161
163
|
"Fuses",
|
|
162
164
|
data.fuses,
|
|
163
165
|
data.fuse_markets,
|
|
164
166
|
data.market_substrates,
|
|
165
|
-
|
|
166
|
-
api_key,
|
|
167
|
+
resolve_name,
|
|
167
168
|
)
|
|
168
169
|
click.echo()
|
|
169
170
|
|
|
@@ -172,18 +173,17 @@ def _print_markets(data: VaultData, api_key: str | None) -> None:
|
|
|
172
173
|
data.instant_fuses,
|
|
173
174
|
data.fuse_markets,
|
|
174
175
|
data.market_substrates,
|
|
175
|
-
|
|
176
|
-
api_key,
|
|
176
|
+
resolve_name,
|
|
177
177
|
)
|
|
178
178
|
click.echo()
|
|
179
179
|
|
|
180
|
-
_print_balance_fuses_table(data,
|
|
180
|
+
_print_balance_fuses_table(data, resolve_name)
|
|
181
181
|
click.echo()
|
|
182
182
|
|
|
183
183
|
_print_dependency_graph(data)
|
|
184
184
|
|
|
185
185
|
click.echo("Substrates per Market:")
|
|
186
|
-
_print_market_substrates(data,
|
|
186
|
+
_print_market_substrates(data, resolve_name)
|
|
187
187
|
click.echo()
|
|
188
188
|
|
|
189
189
|
|
|
@@ -235,8 +235,7 @@ def _print_fuse_section(
|
|
|
235
235
|
fuses: Sequence[FuseAddress],
|
|
236
236
|
fuse_markets: dict[FuseAddress, MarketId] | None,
|
|
237
237
|
market_substrates: dict[MarketId, list[bytes]] | None,
|
|
238
|
-
|
|
239
|
-
api_key: str | None,
|
|
238
|
+
resolve_name: NameResolver,
|
|
240
239
|
) -> None:
|
|
241
240
|
"""Render a fuse section (regular or instant-withdrawal) with a unified
|
|
242
241
|
layout.
|
|
@@ -262,10 +261,7 @@ def _print_fuse_section(
|
|
|
262
261
|
return
|
|
263
262
|
|
|
264
263
|
with ThreadPoolExecutor() as pool:
|
|
265
|
-
name_futs = {
|
|
266
|
-
addr: pool.submit(get_contract_name, chain_id, addr, api_key)
|
|
267
|
-
for addr in counts
|
|
268
|
-
}
|
|
264
|
+
name_futs = {addr: pool.submit(resolve_name, addr) for addr in counts}
|
|
269
265
|
rows: list[tuple[str, ...]] = []
|
|
270
266
|
for idx, addr in enumerate(counts, 1):
|
|
271
267
|
market_label = "?"
|
|
@@ -285,7 +281,7 @@ def _print_fuse_section(
|
|
|
285
281
|
_print_table(("#", "Address", "Contract", "Market", "Substrates"), rows)
|
|
286
282
|
|
|
287
283
|
|
|
288
|
-
def _print_balance_fuses_table(data: VaultData,
|
|
284
|
+
def _print_balance_fuses_table(data: VaultData, resolve_name: NameResolver) -> None:
|
|
289
285
|
balance_fuses = data.balance_fuses
|
|
290
286
|
decimals = data.asset_decimals
|
|
291
287
|
asset_symbol = data.asset_symbol
|
|
@@ -294,9 +290,7 @@ def _print_balance_fuses_table(data: VaultData, api_key: str | None) -> None:
|
|
|
294
290
|
futures: list[tuple[int, MarketId, str, Future]] = []
|
|
295
291
|
for idx, balance_fuse in enumerate(balance_fuses, 1):
|
|
296
292
|
market_id_str = format_market_label(balance_fuse.market_id)
|
|
297
|
-
f_contract = pool.submit(
|
|
298
|
-
get_contract_name, data.chain_id, balance_fuse.fuse, api_key
|
|
299
|
-
)
|
|
293
|
+
f_contract = pool.submit(resolve_name, balance_fuse.fuse)
|
|
300
294
|
futures.append((idx, balance_fuse.market_id, market_id_str, f_contract))
|
|
301
295
|
|
|
302
296
|
venue_rows: list[tuple[str, ...]] = []
|
|
@@ -328,18 +322,18 @@ def _print_balance_fuses_table(data: VaultData, api_key: str | None) -> None:
|
|
|
328
322
|
_print_table(header, [(str(i), *row) for i, row in enumerate(zero_rows, 1)])
|
|
329
323
|
|
|
330
324
|
|
|
331
|
-
def _print_market_substrates(data: VaultData,
|
|
325
|
+
def _print_market_substrates(data: VaultData, resolve_name: NameResolver) -> set[str]:
|
|
332
326
|
if GET_MARKET_SUBSTRATES in data.unimplemented_getters:
|
|
333
327
|
click.secho(
|
|
334
328
|
f" (unavailable: the vault predates {GET_MARKET_SUBSTRATES})",
|
|
335
329
|
fg="yellow",
|
|
336
330
|
)
|
|
337
331
|
return set()
|
|
338
|
-
return _print_substrates(data,
|
|
332
|
+
return _print_substrates(data, resolve_name)
|
|
339
333
|
|
|
340
334
|
|
|
341
335
|
def _print_substrates( # noqa: C901
|
|
342
|
-
data: VaultData,
|
|
336
|
+
data: VaultData, resolve_name: NameResolver
|
|
343
337
|
) -> set[str]:
|
|
344
338
|
substrates_by_market = data.market_substrates or {}
|
|
345
339
|
balance_fuses = data.balance_fuses
|
|
@@ -364,8 +358,7 @@ def _print_substrates( # noqa: C901
|
|
|
364
358
|
resolved_symbols = data.token_symbols
|
|
365
359
|
with ThreadPoolExecutor() as pool:
|
|
366
360
|
contract_futs = {
|
|
367
|
-
addr: pool.submit(
|
|
368
|
-
for addr in all_addresses
|
|
361
|
+
addr: pool.submit(resolve_name, addr) for addr in all_addresses
|
|
369
362
|
}
|
|
370
363
|
resolved_contracts = {addr: fut.result() for addr, fut in contract_futs.items()}
|
|
371
364
|
|
|
@@ -18,6 +18,7 @@ from web3.providers.rpc.utils import (
|
|
|
18
18
|
)
|
|
19
19
|
from web3.types import BlockIdentifier, LogReceipt, TxParams, TxReceipt, Wei
|
|
20
20
|
|
|
21
|
+
from ipor_fusion.cache import ContractCache, default_contract_cache
|
|
21
22
|
from ipor_fusion.chains import GET_LOGS_RANGE_HINTS
|
|
22
23
|
from ipor_fusion.core.logs import (
|
|
23
24
|
DEFAULT_LOG_SCAN_MAX_REQUESTS,
|
|
@@ -81,6 +82,7 @@ class Web3Context:
|
|
|
81
82
|
log_scan_timeout_s: float = DEFAULT_LOG_SCAN_TIMEOUT_S,
|
|
82
83
|
log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
|
|
83
84
|
log_source: LogSource | None = None,
|
|
85
|
+
cache: ContractCache | None = None,
|
|
84
86
|
):
|
|
85
87
|
self._web3 = web3
|
|
86
88
|
self._chain_id = chain_id
|
|
@@ -90,6 +92,8 @@ class Web3Context:
|
|
|
90
92
|
self.log_scan_max_requests = log_scan_max_requests
|
|
91
93
|
# Asked first by `get_logs`; None scans the RPC only.
|
|
92
94
|
self.log_source = log_source
|
|
95
|
+
# None: the process-wide file cache (`cache.default_contract_cache`).
|
|
96
|
+
self._cache = cache
|
|
93
97
|
self._private_key = private_key
|
|
94
98
|
self._gas_multiplier = gas_multiplier
|
|
95
99
|
self._default_block: BlockIdentifier = "latest"
|
|
@@ -109,6 +113,14 @@ class Web3Context:
|
|
|
109
113
|
def chain_id(self) -> ChainId:
|
|
110
114
|
return self._chain_id
|
|
111
115
|
|
|
116
|
+
@property
|
|
117
|
+
def cache(self) -> ContractCache:
|
|
118
|
+
"""Where the SDK keeps immutable contract facts read through this
|
|
119
|
+
context (token symbols and decimals, contract names, deployment
|
|
120
|
+
blocks): the ``cache=`` passed in, else the file cache under
|
|
121
|
+
``$XDG_CACHE_HOME/ipor-fusion/``. Pass `NullCache()` to keep nothing."""
|
|
122
|
+
return self._cache if self._cache is not None else default_contract_cache()
|
|
123
|
+
|
|
112
124
|
@property
|
|
113
125
|
def default_block(self) -> BlockIdentifier:
|
|
114
126
|
return self._default_block
|
|
@@ -119,7 +131,7 @@ class Web3Context:
|
|
|
119
131
|
|
|
120
132
|
def at_block(self, block: BlockIdentifier) -> Web3Context:
|
|
121
133
|
"""A copy of this context whose reads default to ``block``: same
|
|
122
|
-
connection, signer, log source and scan bounds. Pinning a copy leaves
|
|
134
|
+
connection, signer, log source, cache and scan bounds. Pinning a copy leaves
|
|
123
135
|
this context (and anything sharing it across threads) untouched."""
|
|
124
136
|
pinned = copy.copy(self)
|
|
125
137
|
pinned._default_block = block
|
|
@@ -140,6 +152,7 @@ class Web3Context:
|
|
|
140
152
|
log_scan_timeout_s: float = DEFAULT_LOG_SCAN_TIMEOUT_S,
|
|
141
153
|
log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
|
|
142
154
|
log_source: LogSource | None = None,
|
|
155
|
+
cache: ContractCache | None = None,
|
|
143
156
|
) -> Web3Context:
|
|
144
157
|
web3 = Web3(
|
|
145
158
|
Web3.HTTPProvider(
|
|
@@ -168,6 +181,7 @@ class Web3Context:
|
|
|
168
181
|
log_scan_timeout_s=log_scan_timeout_s,
|
|
169
182
|
log_scan_max_requests=log_scan_max_requests,
|
|
170
183
|
log_source=log_source,
|
|
184
|
+
cache=cache,
|
|
171
185
|
)
|
|
172
186
|
|
|
173
187
|
def call(
|