ipor-fusion 4.0.0.dev106__tar.gz → 4.0.0.dev108__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.dev108}/AGENTS.md +12 -3
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/MIGRATION_4.md +57 -3
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/PKG-INFO +11 -3
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/README.md +10 -2
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/__init__.py +16 -0
- ipor_fusion-4.0.0.dev108/src/ipor_fusion/cache.py +204 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_info_view.py +4 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/context.py +15 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/explorer.py +23 -11
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/server.py +4 -1
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_data.py +60 -47
- ipor_fusion-4.0.0.dev108/tests/test_cache.py +316 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_commands.py +12 -6
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_helpers.py +15 -45
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_market_morpho_blue.py +1 -8
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_info_golden.py +7 -11
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_explorer.py +38 -18
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_live.py +1 -5
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_public_api.py +7 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/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.dev108}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/access.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/contract.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/discovery.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/models.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/morpho_api.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/types.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_info.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_managers.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_positions.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_reads.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/conftest.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.txt +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.json +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.txt +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_pending_requests_scan.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_predates_getters.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_contract_call.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_indexer.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_quote.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_lane.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_transport.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_error_hierarchy.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fusion_instance.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_readers.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_models.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_server.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_reader_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_validation.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_simulator_blocks.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/typed_client/rejected.py +0 -0
- {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/uv.lock +0 -0
|
@@ -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,61 @@ 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. `build_vault_info_json` without `resolve_name` still names
|
|
355
|
+
contracts from the default file cache; pass `resolve_name` to avoid it.
|
|
356
|
+
- `ipor_fusion.cache` is a public module now and exports `ContractCache`,
|
|
357
|
+
`FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
|
|
358
|
+
`default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
|
|
359
|
+
are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
|
|
360
|
+
`load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
|
|
361
|
+
`DEPLOYMENT_CACHE_FILE`. `cache_key(chain_id, address, field)` returns a `CacheKey`
|
|
362
|
+
and raises `ValueError` for an address that is not 20 bytes of hex, so
|
|
363
|
+
`get_contract_name` does too.
|
|
364
|
+
|
|
365
|
+
**File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
|
|
366
|
+
`symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
|
|
367
|
+
load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
|
|
368
|
+
3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
|
|
369
|
+
discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
|
|
370
|
+
is no longer read: a vault's deployment block is resolved again (it needs an
|
|
371
|
+
Etherscan key) and the old file can be deleted.
|
|
372
|
+
|
|
373
|
+
**Behavior.**
|
|
374
|
+
|
|
375
|
+
- The same address on two chains has two entries: a token's symbol or decimals read
|
|
376
|
+
on one chain is never served for another.
|
|
377
|
+
- Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
|
|
378
|
+
address without code, but no longer stores it, so a token deployed later (or seen at
|
|
379
|
+
a later block) resolves.
|
|
380
|
+
- A write merges with the file as it is on disk and replaces it atomically (a
|
|
381
|
+
temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
|
|
382
|
+
processes writing at once never leave a torn file; one of two concurrent writers may
|
|
383
|
+
lose its newest entry, which is read again later.
|
|
384
|
+
- A cache never fails a read: a corrupt or unreadable file reads as empty (logged
|
|
385
|
+
warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
|
|
386
|
+
directory (read-only file system) keeps the entries in memory for the process; a
|
|
387
|
+
custom cache that raises is treated as a miss.
|
|
388
|
+
- `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
|
|
389
|
+
transaction or its block cannot be read; a cache failure is no longer reported as
|
|
390
|
+
one (the deployment block is returned).
|
|
391
|
+
|
|
337
392
|
## Behavior changes
|
|
338
393
|
|
|
339
394
|
- Two fuses are equal (and hash alike) only when they have the same class and the
|
|
@@ -371,9 +426,8 @@ gets it from `call.build_transaction(ctx)`.
|
|
|
371
426
|
- `TransactionError.tx_hash` is `0x`-prefixed.
|
|
372
427
|
- Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
|
|
373
428
|
a value that is not an address.
|
|
374
|
-
- The
|
|
375
|
-
|
|
376
|
-
for the other.
|
|
429
|
+
- The contract cache keys every entry by chain and lowercase address and no longer
|
|
430
|
+
caches an absence (see [Contract cache](#contract-cache)).
|
|
377
431
|
|
|
378
432
|
## Errors
|
|
379
433
|
|
|
@@ -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.dev108
|
|
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.dev108}/src/ipor_fusion/cli/vault_info_view.py
RENAMED
|
@@ -87,7 +87,10 @@ def _print_vault_info(
|
|
|
87
87
|
|
|
88
88
|
if json_output:
|
|
89
89
|
result = build_vault_info_json(
|
|
90
|
-
data,
|
|
90
|
+
data,
|
|
91
|
+
resolve_name=partial(
|
|
92
|
+
get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
|
|
93
|
+
),
|
|
91
94
|
)
|
|
92
95
|
click.echo(json.dumps(result, indent=2))
|
|
93
96
|
return
|
|
@@ -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(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Etherscan V2 client: contract names (cached
|
|
2
|
-
transactions. Every lookup needs an API key; without one,
|
|
3
|
-
cache only."""
|
|
1
|
+
"""Etherscan V2 client: contract names (cached in a `ContractCache`) and
|
|
2
|
+
contract creation transactions. Every lookup needs an API key; without one,
|
|
3
|
+
names come from the cache only."""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -12,7 +12,13 @@ from urllib.request import urlopen
|
|
|
12
12
|
|
|
13
13
|
from eth_typing import HexAddress
|
|
14
14
|
|
|
15
|
-
from ipor_fusion.cache import
|
|
15
|
+
from ipor_fusion.cache import (
|
|
16
|
+
ContractCache,
|
|
17
|
+
cache_get,
|
|
18
|
+
cache_key,
|
|
19
|
+
cache_put,
|
|
20
|
+
default_contract_cache,
|
|
21
|
+
)
|
|
16
22
|
from ipor_fusion.types import ChainId, TxHash, parse_tx_hash
|
|
17
23
|
|
|
18
24
|
ETHERSCAN_V2_URL = "https://api.etherscan.io/v2/api"
|
|
@@ -43,16 +49,22 @@ _etherscan_limiter = _RateLimiter(2.5)
|
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def get_contract_name(
|
|
46
|
-
chain_id: ChainId,
|
|
52
|
+
chain_id: ChainId,
|
|
53
|
+
address: HexAddress,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
*,
|
|
56
|
+
cache: ContractCache | None = None,
|
|
47
57
|
) -> str:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
"""The verified contract name at ``address``, ``""`` when unknown. Read from
|
|
59
|
+
``cache`` first (default: the file cache, `default_contract_cache`; pass a
|
|
60
|
+
context's ``ctx.cache`` to share its policy), else from Etherscan when
|
|
61
|
+
``api_key`` is set; a name found there is cached, its absence is not."""
|
|
62
|
+
store = default_contract_cache() if cache is None else cache
|
|
63
|
+
key = cache_key(chain_id, address, "name")
|
|
64
|
+
if cached := cache_get(store, [key]).get(key):
|
|
52
65
|
return cached
|
|
53
|
-
|
|
54
66
|
if name := _fetch_contract_name(chain_id, address, api_key):
|
|
55
|
-
|
|
67
|
+
cache_put(store, {key: name})
|
|
56
68
|
return name
|
|
57
69
|
return ""
|
|
58
70
|
|
|
@@ -210,7 +210,10 @@ def vault_info(
|
|
|
210
210
|
) = fetch_deployment_info(ctx, checksum, api_key=api_key)
|
|
211
211
|
|
|
212
212
|
result = build_vault_info_json(
|
|
213
|
-
data,
|
|
213
|
+
data,
|
|
214
|
+
resolve_name=partial(
|
|
215
|
+
get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
|
|
216
|
+
),
|
|
214
217
|
)
|
|
215
218
|
return VaultInfoResponse.model_validate(result)
|
|
216
219
|
|