ipor-fusion 4.0.0.dev41__tar.gz → 4.0.0.dev45__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.dev41 → ipor_fusion-4.0.0.dev45}/PKG-INFO +1 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cache.py +32 -12
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/vault_cmd.py +1 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/explorer.py +4 -4
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/mcp/server.py +6 -10
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/morpho_api.py +14 -12
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/vault_data.py +11 -10
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/clean.json +2 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/clean.txt +1 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/rich.json +4 -2
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/rich.txt +3 -3
- ipor_fusion-4.0.0.dev45/tests/test_cache.py +99 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_market_morpho_blue.py +30 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_vault_info_golden.py +5 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_explorer.py +1 -1
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_mcp_server.py +19 -6
- ipor_fusion-4.0.0.dev41/tests/test_cache.py +0 -44
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/AGENTS.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/README.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/vault_health.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/vault_info_view.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/access.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/context.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/contract.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/multicall.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/discovery.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/errors.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/mcp/models.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/types.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/validation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/src/ipor_fusion/vault_info.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/conftest.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_commands.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_helpers.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_pending_requests_scan.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_vault_predates_getters.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_contract_call.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_ccip_indexer.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_ccip_quote.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_lane.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_crosschain_transport.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_live.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_readers.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_mcp_models.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_reader_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_validation.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_vault_simulator_blocks.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/typed_client/consumer.py +0 -0
- {ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/uv.lock +0 -0
|
@@ -9,6 +9,8 @@ import os
|
|
|
9
9
|
import threading
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
from eth_typing import HexAddress
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def _xdg_cache_home() -> Path:
|
|
14
16
|
return Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
@@ -21,11 +23,35 @@ DEPLOYMENT_CACHE_FILE = CACHE_DIR / "deployment_cache.json"
|
|
|
21
23
|
_cache_lock = threading.Lock()
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def cache_key(scope: object, address: HexAddress) -> str:
|
|
27
|
+
"""``<scope>:<address>`` with the address lowercased: one entry per address
|
|
28
|
+
whatever spelling the caller holds. ``scope`` is a chain id or a field name
|
|
29
|
+
(``symbol``, ``decimals``)."""
|
|
30
|
+
return f"{scope}:{address.lower()}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _canonical(entries: dict) -> dict:
|
|
34
|
+
"""Entries keyed by `cache_key` spelling. Caches written before 4.0 keyed
|
|
35
|
+
the address as the caller spelled it; an entry already canonical wins over
|
|
36
|
+
a differently spelled duplicate."""
|
|
37
|
+
out: dict = {}
|
|
38
|
+
for key, value in entries.items():
|
|
39
|
+
scope, sep, address = key.rpartition(":")
|
|
40
|
+
canonical = cache_key(scope, address) if sep else key
|
|
41
|
+
if canonical == key or canonical not in out:
|
|
42
|
+
out[canonical] = value
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read(path: Path) -> dict:
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return {}
|
|
49
|
+
return _canonical(json.loads(path.read_text(encoding="utf-8")))
|
|
50
|
+
|
|
51
|
+
|
|
24
52
|
def load_contract_cache() -> dict[str, str]:
|
|
25
53
|
with _cache_lock:
|
|
26
|
-
|
|
27
|
-
return {}
|
|
28
|
-
return json.loads(CACHE_FILE.read_text(encoding="utf-8")) # type: ignore[no-any-return]
|
|
54
|
+
return _read(CACHE_FILE)
|
|
29
55
|
|
|
30
56
|
|
|
31
57
|
def save_contract_cache(cache: dict[str, str]) -> None:
|
|
@@ -37,9 +63,7 @@ def save_contract_cache(cache: dict[str, str]) -> None:
|
|
|
37
63
|
def update_contract_cache(key: str, value: str) -> None:
|
|
38
64
|
with _cache_lock:
|
|
39
65
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
existing: dict[str, str] =
|
|
41
|
-
if CACHE_FILE.exists():
|
|
42
|
-
existing = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
66
|
+
existing: dict[str, str] = _read(CACHE_FILE)
|
|
43
67
|
existing[key] = value
|
|
44
68
|
CACHE_FILE.write_text(json.dumps(existing, indent=2), encoding="utf-8")
|
|
45
69
|
|
|
@@ -49,17 +73,13 @@ _deployment_cache_lock = threading.Lock()
|
|
|
49
73
|
|
|
50
74
|
def load_deployment_cache() -> dict[str, dict[str, int]]:
|
|
51
75
|
with _deployment_cache_lock:
|
|
52
|
-
|
|
53
|
-
return {}
|
|
54
|
-
return json.loads(DEPLOYMENT_CACHE_FILE.read_text(encoding="utf-8")) # type: ignore[no-any-return]
|
|
76
|
+
return _read(DEPLOYMENT_CACHE_FILE)
|
|
55
77
|
|
|
56
78
|
|
|
57
79
|
def update_deployment_cache(key: str, block: int, timestamp: int) -> None:
|
|
58
80
|
with _deployment_cache_lock:
|
|
59
81
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
existing: dict[str, dict[str, int]] =
|
|
61
|
-
if DEPLOYMENT_CACHE_FILE.exists():
|
|
62
|
-
existing = json.loads(DEPLOYMENT_CACHE_FILE.read_text(encoding="utf-8"))
|
|
82
|
+
existing: dict[str, dict[str, int]] = _read(DEPLOYMENT_CACHE_FILE)
|
|
63
83
|
existing[key] = {"block": block, "timestamp": timestamp}
|
|
64
84
|
DEPLOYMENT_CACHE_FILE.write_text(
|
|
65
85
|
json.dumps(existing, indent=2), encoding="utf-8"
|
|
@@ -131,7 +131,7 @@ def add(address: ChecksumAddress, label: str | None, chain_id: ChainId | None) -
|
|
|
131
131
|
|
|
132
132
|
@vault.command("remove")
|
|
133
133
|
@click.argument("address", type=ADDRESS)
|
|
134
|
-
def remove(address:
|
|
134
|
+
def remove(address: ChecksumAddress) -> None:
|
|
135
135
|
"""Remove a saved vault."""
|
|
136
136
|
cfg = load_config()
|
|
137
137
|
before = len(cfg.vaults)
|
|
@@ -12,7 +12,7 @@ from urllib.request import urlopen
|
|
|
12
12
|
|
|
13
13
|
from eth_typing import HexAddress, HexStr
|
|
14
14
|
|
|
15
|
-
from ipor_fusion.cache import load_contract_cache, update_contract_cache
|
|
15
|
+
from ipor_fusion.cache import cache_key, load_contract_cache, update_contract_cache
|
|
16
16
|
from ipor_fusion.types import ChainId, TxHash
|
|
17
17
|
|
|
18
18
|
ETHERSCAN_V2_URL = "https://api.etherscan.io/v2/api"
|
|
@@ -44,13 +44,13 @@ def get_contract_name(
|
|
|
44
44
|
chain_id: ChainId, address: HexAddress, api_key: str | None = None
|
|
45
45
|
) -> str:
|
|
46
46
|
cache = load_contract_cache()
|
|
47
|
-
|
|
47
|
+
key = cache_key(chain_id, address)
|
|
48
48
|
|
|
49
|
-
if cached := cache.get(
|
|
49
|
+
if cached := cache.get(key):
|
|
50
50
|
return cached
|
|
51
51
|
|
|
52
52
|
if name := _fetch_contract_name(chain_id, address, api_key):
|
|
53
|
-
update_contract_cache(
|
|
53
|
+
update_contract_cache(key, name)
|
|
54
54
|
return name
|
|
55
55
|
return ""
|
|
56
56
|
|
|
@@ -437,6 +437,7 @@ def vault_add(
|
|
|
437
437
|
label: Human-readable label (fetched from on-chain name() if empty).
|
|
438
438
|
chain_id: Chain ID (auto-detected when only one provider configured).
|
|
439
439
|
"""
|
|
440
|
+
checksum = Web3.to_checksum_address(address)
|
|
440
441
|
cfg = load_config()
|
|
441
442
|
|
|
442
443
|
if not chain_id:
|
|
@@ -451,26 +452,21 @@ def vault_add(
|
|
|
451
452
|
if not label:
|
|
452
453
|
provider_url = _resolve_provider(cfg, chain_id)
|
|
453
454
|
ctx = Web3Context.from_url(provider_url)
|
|
454
|
-
checksum = Web3.to_checksum_address(address)
|
|
455
455
|
try:
|
|
456
456
|
label = PlasmaVault(ctx, checksum).name().call()
|
|
457
457
|
except Exception:
|
|
458
458
|
label = checksum
|
|
459
459
|
|
|
460
460
|
for vault_entry in cfg.vaults:
|
|
461
|
-
if vault_entry.address.lower() ==
|
|
461
|
+
if vault_entry.address.lower() == checksum.lower():
|
|
462
462
|
vault_entry.label = label
|
|
463
463
|
vault_entry.chain_id = chain_id
|
|
464
464
|
save_config(cfg)
|
|
465
|
-
return ActionResult(message=f"Vault {label} ({
|
|
465
|
+
return ActionResult(message=f"Vault {label} ({checksum}) updated.")
|
|
466
466
|
|
|
467
|
-
cfg.vaults.append(
|
|
468
|
-
VaultEntry(
|
|
469
|
-
address=Web3.to_checksum_address(address), label=label, chain_id=chain_id
|
|
470
|
-
)
|
|
471
|
-
)
|
|
467
|
+
cfg.vaults.append(VaultEntry(address=checksum, label=label, chain_id=chain_id))
|
|
472
468
|
save_config(cfg)
|
|
473
|
-
return ActionResult(message=f"Vault {label} ({
|
|
469
|
+
return ActionResult(message=f"Vault {label} ({checksum}) added.")
|
|
474
470
|
|
|
475
471
|
|
|
476
472
|
@mcp.tool()
|
|
@@ -635,7 +631,7 @@ def market_meta_morpho(
|
|
|
635
631
|
chain_id: EVM chain ID.
|
|
636
632
|
"""
|
|
637
633
|
try:
|
|
638
|
-
info = fetch_vault(vault_address, chain_id)
|
|
634
|
+
info = fetch_vault(Web3.to_checksum_address(vault_address), chain_id)
|
|
639
635
|
except MorphoApiError as exc:
|
|
640
636
|
raise ValueError(str(exc)) from exc
|
|
641
637
|
payload = build_meta_morpho_json(info, chain_id)
|
|
@@ -91,7 +91,7 @@ class VaultFlowCap:
|
|
|
91
91
|
fee_wei: int
|
|
92
92
|
max_in: int
|
|
93
93
|
max_out: int
|
|
94
|
-
admin:
|
|
94
|
+
admin: ChecksumAddress | None # the address that can configure these flow caps
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
@dataclass(slots=True)
|
|
@@ -106,7 +106,7 @@ class VaultAllocation:
|
|
|
106
106
|
total_assets: int
|
|
107
107
|
supply_assets: int # currently allocated to *this* market
|
|
108
108
|
supply_cap: int # cap configured by curator for *this* market
|
|
109
|
-
allocators: list[
|
|
109
|
+
allocators: list[ChecksumAddress] # addresses with ALLOCATOR_ROLE on the vault
|
|
110
110
|
flow_cap: VaultFlowCap | None # None if PublicAllocator not configured
|
|
111
111
|
|
|
112
112
|
|
|
@@ -122,8 +122,8 @@ class MorphoApiMarket:
|
|
|
122
122
|
collateral_token: ChecksumAddress
|
|
123
123
|
collateral_symbol: str
|
|
124
124
|
collateral_decimals: int
|
|
125
|
-
oracle:
|
|
126
|
-
irm:
|
|
125
|
+
oracle: ChecksumAddress
|
|
126
|
+
irm: ChecksumAddress
|
|
127
127
|
supply_assets: int
|
|
128
128
|
borrow_assets: int
|
|
129
129
|
liquidity_assets: int
|
|
@@ -190,8 +190,8 @@ def _parse_market(raw: dict) -> MorphoApiMarket:
|
|
|
190
190
|
collateral_token=_api_address(raw["collateralAsset"]["address"]),
|
|
191
191
|
collateral_symbol=raw["collateralAsset"]["symbol"],
|
|
192
192
|
collateral_decimals=int(raw["collateralAsset"]["decimals"]),
|
|
193
|
-
oracle=raw["oracle"]["address"],
|
|
194
|
-
irm=raw["irmAddress"],
|
|
193
|
+
oracle=_api_address(raw["oracle"]["address"]),
|
|
194
|
+
irm=_api_address(raw["irmAddress"]),
|
|
195
195
|
supply_assets=int(state.get("supplyAssets") or 0),
|
|
196
196
|
borrow_assets=int(state.get("borrowAssets") or 0),
|
|
197
197
|
liquidity_assets=int(state.get("liquidityAssets") or 0),
|
|
@@ -283,8 +283,8 @@ class VaultV2Cap:
|
|
|
283
283
|
market_id: MorphoBlueMarketId | None
|
|
284
284
|
loan_token: ChecksumAddress | None
|
|
285
285
|
collateral_token: ChecksumAddress | None
|
|
286
|
-
oracle:
|
|
287
|
-
irm:
|
|
286
|
+
oracle: ChecksumAddress | None
|
|
287
|
+
irm: ChecksumAddress | None
|
|
288
288
|
lltv: int | None
|
|
289
289
|
|
|
290
290
|
@property
|
|
@@ -378,7 +378,7 @@ class VaultV1Info:
|
|
|
378
378
|
|
|
379
379
|
|
|
380
380
|
def fetch_vault(
|
|
381
|
-
address:
|
|
381
|
+
address: ChecksumAddress, chain_id: ChainId, *, timeout: float = 10.0
|
|
382
382
|
) -> VaultV2Info | VaultV1Info:
|
|
383
383
|
"""Fetch a Morpho vault by address. Tries V2 first, falls back to V1.
|
|
384
384
|
|
|
@@ -625,7 +625,7 @@ def _parse_vault_v1(raw: dict) -> VaultV1Info:
|
|
|
625
625
|
fee_wei=int(pac.get("fee") or 0),
|
|
626
626
|
max_in=0,
|
|
627
627
|
max_out=0,
|
|
628
|
-
admin=pac.get("admin"),
|
|
628
|
+
admin=_api_optional_address(pac.get("admin")),
|
|
629
629
|
)
|
|
630
630
|
for fc in pac.get("flowCaps") or []:
|
|
631
631
|
if raw_mid := (fc.get("market") or {}).get("marketId"):
|
|
@@ -687,10 +687,12 @@ def _parse_vault(raw: dict, market_id: MorphoBlueMarketId) -> VaultAllocation:
|
|
|
687
687
|
fee_wei=int(pac.get("fee") or 0),
|
|
688
688
|
max_in=int(flow_for_market.get("maxIn") or 0),
|
|
689
689
|
max_out=int(flow_for_market.get("maxOut") or 0),
|
|
690
|
-
admin=pac.get("admin"),
|
|
690
|
+
admin=_api_optional_address(pac.get("admin")),
|
|
691
691
|
)
|
|
692
692
|
allocators = [
|
|
693
|
-
a["address"]
|
|
693
|
+
_api_address(a["address"])
|
|
694
|
+
for a in (raw.get("allocators") or [])
|
|
695
|
+
if a.get("address")
|
|
694
696
|
]
|
|
695
697
|
return VaultAllocation(
|
|
696
698
|
vault_address=_api_address(raw["address"]),
|
|
@@ -25,6 +25,7 @@ from web3.exceptions import ContractLogicError, TimeExhausted, Web3RPCError
|
|
|
25
25
|
from web3.types import ChecksumAddress, HexStr
|
|
26
26
|
|
|
27
27
|
from ipor_fusion.cache import (
|
|
28
|
+
cache_key,
|
|
28
29
|
load_contract_cache,
|
|
29
30
|
load_deployment_cache,
|
|
30
31
|
update_contract_cache,
|
|
@@ -325,14 +326,14 @@ def resolve_token_symbol(ctx: Web3Context, address: HexAddress) -> str:
|
|
|
325
326
|
"""ERC-20 symbol, cached; ``"no contract"`` when nothing is deployed at
|
|
326
327
|
``address`` and ``""`` when the symbol cannot be read."""
|
|
327
328
|
cache = load_contract_cache()
|
|
328
|
-
|
|
329
|
-
if cached := cache.get(
|
|
329
|
+
key = cache_key("symbol", address)
|
|
330
|
+
if cached := cache.get(key):
|
|
330
331
|
return cached
|
|
331
332
|
|
|
332
333
|
checksum = Web3.to_checksum_address(address)
|
|
333
334
|
code = ctx.web3.eth.get_code(checksum)
|
|
334
335
|
if not code or code == b"":
|
|
335
|
-
update_contract_cache(
|
|
336
|
+
update_contract_cache(key, _NO_CONTRACT)
|
|
336
337
|
return _NO_CONTRACT
|
|
337
338
|
|
|
338
339
|
try:
|
|
@@ -340,15 +341,15 @@ def resolve_token_symbol(ctx: Web3Context, address: HexAddress) -> str:
|
|
|
340
341
|
except Exception:
|
|
341
342
|
symbol = ""
|
|
342
343
|
if symbol:
|
|
343
|
-
update_contract_cache(
|
|
344
|
+
update_contract_cache(key, symbol)
|
|
344
345
|
return symbol
|
|
345
346
|
|
|
346
347
|
|
|
347
348
|
def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> int | None:
|
|
348
349
|
"""Resolve ERC-20 decimals, cached. Returns None if address is not a contract."""
|
|
349
350
|
cache = load_contract_cache()
|
|
350
|
-
|
|
351
|
-
if cached := cache.get(
|
|
351
|
+
key = cache_key("decimals", address)
|
|
352
|
+
if cached := cache.get(key):
|
|
352
353
|
return int(cached)
|
|
353
354
|
|
|
354
355
|
checksum = Web3.to_checksum_address(address)
|
|
@@ -360,7 +361,7 @@ def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> int | None:
|
|
|
360
361
|
decimals = ERC20(ctx, checksum).decimals().call()
|
|
361
362
|
except Exception:
|
|
362
363
|
return None
|
|
363
|
-
update_contract_cache(
|
|
364
|
+
update_contract_cache(key, str(decimals))
|
|
364
365
|
return decimals
|
|
365
366
|
|
|
366
367
|
|
|
@@ -1053,9 +1054,9 @@ def fetch_deployment_info(
|
|
|
1053
1054
|
Base/Optimism on the free Etherscan tier). ``None`` means either success or
|
|
1054
1055
|
a benign empty response.
|
|
1055
1056
|
"""
|
|
1056
|
-
|
|
1057
|
+
key = cache_key(chain_id, vault_address)
|
|
1057
1058
|
cache = load_deployment_cache()
|
|
1058
|
-
if entry := cache.get(
|
|
1059
|
+
if entry := cache.get(key):
|
|
1059
1060
|
return entry["block"], entry["timestamp"], None
|
|
1060
1061
|
|
|
1061
1062
|
tx_hash, error = get_deployment_tx(chain_id, vault_address, api_key)
|
|
@@ -1067,7 +1068,7 @@ def fetch_deployment_info(
|
|
|
1067
1068
|
block_number: int = tx["blockNumber"]
|
|
1068
1069
|
block_info = ctx.web3.eth.get_block(block_number)
|
|
1069
1070
|
timestamp: int = block_info["timestamp"]
|
|
1070
|
-
update_deployment_cache(
|
|
1071
|
+
update_deployment_cache(key, block_number, timestamp)
|
|
1071
1072
|
return block_number, timestamp, None
|
|
1072
1073
|
except Exception:
|
|
1073
1074
|
return None, None, "rpc-fetch-failed"
|
{ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/clean.txt
RENAMED
|
@@ -36,7 +36,7 @@ Balance Fuses (1):
|
|
|
36
36
|
|
|
37
37
|
Substrates per Market:
|
|
38
38
|
AAVE_V3 (1):
|
|
39
|
-
0x00000000000000000000000000000000000000a1 (symbol=USDC)
|
|
39
|
+
0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
|
|
40
40
|
|
|
41
41
|
ERC20 Balances (vault holdings):
|
|
42
42
|
(no ERC20_VAULT_BALANCE market)
|
{ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/rich.json
RENAMED
|
@@ -143,7 +143,8 @@
|
|
|
143
143
|
"AAVE_V3 (1)": [
|
|
144
144
|
{
|
|
145
145
|
"address": "0x00000000000000000000000000000000000000a1",
|
|
146
|
-
"symbol": "USDC"
|
|
146
|
+
"symbol": "USDC",
|
|
147
|
+
"contract": "FiatTokenProxy"
|
|
147
148
|
}
|
|
148
149
|
],
|
|
149
150
|
"MORPHO (14)": [
|
|
@@ -155,7 +156,8 @@
|
|
|
155
156
|
"ERC20_VAULT_BALANCE (7)": [
|
|
156
157
|
{
|
|
157
158
|
"address": "0x00000000000000000000000000000000000000a1",
|
|
158
|
-
"symbol": "USDC"
|
|
159
|
+
"symbol": "USDC",
|
|
160
|
+
"contract": "FiatTokenProxy"
|
|
159
161
|
},
|
|
160
162
|
{
|
|
161
163
|
"address": "0x00000000000000000000000000000000000000b1",
|
{ipor_fusion-4.0.0.dev41 → ipor_fusion-4.0.0.dev45}/tests/fixtures/vault_info_golden/rich.txt
RENAMED
|
@@ -53,17 +53,17 @@ Dependency Balance Graph:
|
|
|
53
53
|
|
|
54
54
|
Substrates per Market:
|
|
55
55
|
AAVE_V3 (1):
|
|
56
|
-
0x00000000000000000000000000000000000000a1 (symbol=USDC)
|
|
56
|
+
0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
|
|
57
57
|
MORPHO (14):
|
|
58
58
|
0xabababababababababababababababababababababababababababababababab (morpho_market_id)[0m
|
|
59
59
|
ERC20_VAULT_BALANCE (7):
|
|
60
|
-
0x00000000000000000000000000000000000000a1 (symbol=USDC)
|
|
60
|
+
0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
|
|
61
61
|
0x00000000000000000000000000000000000000b1 (symbol=STALE)
|
|
62
62
|
0x00000000000000000000000000000000000000b2 (symbol=NOPRICE)
|
|
63
63
|
0x00000000000000000000000000000000000000b3 (symbol=EMPTY)
|
|
64
64
|
0x00000000000000000000000000000000000000b4 (symbol=BROKEN)
|
|
65
65
|
AAVE_V3 (1):
|
|
66
|
-
0x00000000000000000000000000000000000000a1 (symbol=USDC)
|
|
66
|
+
0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
|
|
67
67
|
|
|
68
68
|
ERC20 Balances (vault holdings):
|
|
69
69
|
Token Symbol Balance Price Note
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ipor_fusion import cache
|
|
6
|
+
from ipor_fusion.cache import load_contract_cache, save_contract_cache
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def _patch_paths(tmp_path, monkeypatch):
|
|
11
|
+
cache_dir = tmp_path / ".cache"
|
|
12
|
+
monkeypatch.setattr(cache, "CACHE_DIR", cache_dir)
|
|
13
|
+
monkeypatch.setattr(cache, "CACHE_FILE", cache_dir / "contract_cache.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestLoadContractCache:
|
|
17
|
+
def test_returns_empty_dict_when_no_file(self):
|
|
18
|
+
assert load_contract_cache() == {}
|
|
19
|
+
|
|
20
|
+
def test_loads_existing_cache(self, tmp_path):
|
|
21
|
+
cache_dir = tmp_path / ".cache"
|
|
22
|
+
cache_dir.mkdir()
|
|
23
|
+
cache = {"0xABC": "ERC20", "0xDEF": "PlasmaVault"}
|
|
24
|
+
(cache_dir / "contract_cache.json").write_text(
|
|
25
|
+
json.dumps(cache), encoding="utf-8"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert load_contract_cache() == cache
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestSaveContractCache:
|
|
32
|
+
def test_creates_directory_and_writes_json(self, tmp_path):
|
|
33
|
+
cache = {"0xABC": "ERC20"}
|
|
34
|
+
save_contract_cache(cache)
|
|
35
|
+
|
|
36
|
+
cache_file = tmp_path / ".cache" / "contract_cache.json"
|
|
37
|
+
assert cache_file.exists()
|
|
38
|
+
assert json.loads(cache_file.read_text(encoding="utf-8")) == cache
|
|
39
|
+
|
|
40
|
+
def test_roundtrip(self):
|
|
41
|
+
original = {"0x111": "TokenA", "0x222": "TokenB"}
|
|
42
|
+
save_contract_cache(original)
|
|
43
|
+
loaded = load_contract_cache()
|
|
44
|
+
assert loaded == original
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestCanonicalKeys:
|
|
48
|
+
CHECKSUM = "0xAbCdEf0123456789aBcDeF0123456789AbCdEf01"
|
|
49
|
+
|
|
50
|
+
def _write(self, tmp_path, name, entries):
|
|
51
|
+
cache_dir = tmp_path / ".cache"
|
|
52
|
+
cache_dir.mkdir(exist_ok=True)
|
|
53
|
+
(cache_dir / name).write_text(json.dumps(entries), encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
def test_a_key_lowercases_only_the_address(self):
|
|
56
|
+
assert cache.cache_key("symbol", self.CHECKSUM) == (
|
|
57
|
+
"symbol:" + self.CHECKSUM.lower()
|
|
58
|
+
)
|
|
59
|
+
assert cache.cache_key(1, self.CHECKSUM) == "1:" + self.CHECKSUM.lower()
|
|
60
|
+
|
|
61
|
+
def test_a_legacy_key_spelled_by_the_caller_still_hits(self, tmp_path):
|
|
62
|
+
self._write(tmp_path, "contract_cache.json", {f"1:{self.CHECKSUM}": "Vault"})
|
|
63
|
+
assert load_contract_cache()[cache.cache_key(1, self.CHECKSUM)] == "Vault"
|
|
64
|
+
|
|
65
|
+
def test_an_already_canonical_entry_wins_over_a_duplicate(self, tmp_path):
|
|
66
|
+
self._write(
|
|
67
|
+
tmp_path,
|
|
68
|
+
"contract_cache.json",
|
|
69
|
+
{
|
|
70
|
+
f"symbol:{self.CHECKSUM.lower()}": "NEW",
|
|
71
|
+
f"symbol:{self.CHECKSUM}": "OLD",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
assert load_contract_cache() == {f"symbol:{self.CHECKSUM.lower()}": "NEW"}
|
|
75
|
+
|
|
76
|
+
def test_an_update_rewrites_legacy_keys(self, tmp_path):
|
|
77
|
+
self._write(tmp_path, "contract_cache.json", {f"1:{self.CHECKSUM}": "Vault"})
|
|
78
|
+
cache.update_contract_cache(cache.cache_key("decimals", self.CHECKSUM), "6")
|
|
79
|
+
on_disk = json.loads(
|
|
80
|
+
(tmp_path / ".cache" / "contract_cache.json").read_text(encoding="utf-8")
|
|
81
|
+
)
|
|
82
|
+
assert on_disk == {
|
|
83
|
+
f"1:{self.CHECKSUM.lower()}": "Vault",
|
|
84
|
+
f"decimals:{self.CHECKSUM.lower()}": "6",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def test_deployment_entries_are_canonical(self, tmp_path, monkeypatch):
|
|
88
|
+
monkeypatch.setattr(
|
|
89
|
+
cache,
|
|
90
|
+
"DEPLOYMENT_CACHE_FILE",
|
|
91
|
+
tmp_path / ".cache" / "deployment_cache.json",
|
|
92
|
+
)
|
|
93
|
+
self._write(
|
|
94
|
+
tmp_path,
|
|
95
|
+
"deployment_cache.json",
|
|
96
|
+
{f"1:{self.CHECKSUM}": {"block": 5, "timestamp": 6}},
|
|
97
|
+
)
|
|
98
|
+
loaded = cache.load_deployment_cache()
|
|
99
|
+
assert loaded[cache.cache_key(1, self.CHECKSUM)] == {"block": 5, "timestamp": 6}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import json
|
|
5
5
|
from unittest.mock import MagicMock, patch
|
|
6
6
|
|
|
7
|
+
import pytest
|
|
7
8
|
from click.testing import CliRunner
|
|
8
9
|
from eth_abi import encode
|
|
9
10
|
from web3 import Web3
|
|
@@ -18,6 +19,7 @@ from ipor_fusion.morpho_api import (
|
|
|
18
19
|
MorphoApiMarket,
|
|
19
20
|
VaultAllocation,
|
|
20
21
|
VaultFlowCap,
|
|
22
|
+
_parse_market,
|
|
21
23
|
fetch_market,
|
|
22
24
|
fetch_vault,
|
|
23
25
|
)
|
|
@@ -786,3 +788,31 @@ class TestMetaMorphoCommand:
|
|
|
786
788
|
assert len(market_caps) == 1
|
|
787
789
|
assert market_caps[0]["loan_token"] == LOAN_TOKEN
|
|
788
790
|
assert market_caps[0]["lltv"] == str(915 * 10**15)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class TestMorphoApiAddresses:
|
|
794
|
+
def _raw(self, oracle: str) -> dict:
|
|
795
|
+
return {
|
|
796
|
+
"marketId": "0x" + MARKET_ID,
|
|
797
|
+
"lltv": "915000000000000000",
|
|
798
|
+
"loanAsset": {"address": LOAN_TOKEN.lower(), "symbol": "U", "decimals": 6},
|
|
799
|
+
"collateralAsset": {
|
|
800
|
+
"address": COLLATERAL_TOKEN,
|
|
801
|
+
"symbol": "C",
|
|
802
|
+
"decimals": 18,
|
|
803
|
+
},
|
|
804
|
+
"oracle": {"address": oracle},
|
|
805
|
+
"irmAddress": IRM.lower(),
|
|
806
|
+
"state": {},
|
|
807
|
+
"supplyingVaults": [],
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
def test_addresses_are_checksummed(self):
|
|
811
|
+
market = _parse_market(self._raw(ORACLE.lower()))
|
|
812
|
+
assert market.loan_token == LOAN_TOKEN
|
|
813
|
+
assert market.oracle == ORACLE
|
|
814
|
+
assert market.irm == IRM
|
|
815
|
+
|
|
816
|
+
def test_a_malformed_address_is_an_api_error(self):
|
|
817
|
+
with pytest.raises(MorphoApiError):
|
|
818
|
+
_parse_market(self._raw("invalid oracle"))
|
|
@@ -362,6 +362,10 @@ def test_vault_info_output_matches_fixture(scenario: str, json_output: bool) ->
|
|
|
362
362
|
json.loads(rendered)
|
|
363
363
|
|
|
364
364
|
|
|
365
|
+
# The CLI's cached lookup matches an address in any spelling; so does this one.
|
|
366
|
+
_NAMES_BY_ADDRESS = {address.lower(): name for address, name in NAMES.items()}
|
|
367
|
+
|
|
368
|
+
|
|
365
369
|
@pytest.mark.usefixtures("isolated_cache")
|
|
366
370
|
@pytest.mark.parametrize("scenario", sorted(SCENARIOS))
|
|
367
371
|
def test_public_builder_renders_the_cli_json(scenario: str) -> None:
|
|
@@ -377,7 +381,7 @@ def test_public_builder_renders_the_cli_json(scenario: str) -> None:
|
|
|
377
381
|
VAULT,
|
|
378
382
|
1,
|
|
379
383
|
"ethereum",
|
|
380
|
-
resolve_name=lambda address:
|
|
384
|
+
resolve_name=lambda address: _NAMES_BY_ADDRESS.get(address.lower(), ""),
|
|
381
385
|
)
|
|
382
386
|
expected = (FIXTURES / f"{scenario}.json").read_text(encoding="utf-8")
|
|
383
387
|
assert json.dumps(result, indent=2) + "\n" == expected
|
|
@@ -59,7 +59,7 @@ class TestGetContractName:
|
|
|
59
59
|
cache_file = tmp_path / ".cache" / "contract_cache.json"
|
|
60
60
|
assert cache_file.exists()
|
|
61
61
|
cached = json.loads(cache_file.read_text(encoding="utf-8"))
|
|
62
|
-
assert cached["1:
|
|
62
|
+
assert cached["1:0xdef"] == "PlasmaVault"
|
|
63
63
|
|
|
64
64
|
def test_returns_empty_for_unsupported_chain(self):
|
|
65
65
|
result = get_contract_name(99999, "0xABC", api_key="key123")
|
|
@@ -89,12 +89,15 @@ def _config_with_keyed_provider():
|
|
|
89
89
|
return FusionConfig(providers={"1": "https://rpc.example.com/v2/SECRETKEY"})
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
_SAVED_VAULT = "0x43ee0243ea8cf02f7087d8b16c8d2007cc9c7ca2"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _config_with_vault(address: str = _SAVED_VAULT):
|
|
93
96
|
from ipor_fusion.cli.config_store import FusionConfig, VaultEntry
|
|
94
97
|
|
|
95
98
|
return FusionConfig(
|
|
96
99
|
providers={"1": "https://rpc.example.com"},
|
|
97
|
-
vaults=[VaultEntry(address=
|
|
100
|
+
vaults=[VaultEntry(address=address, label="Test", chain_id=1)],
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
|
|
@@ -264,7 +267,7 @@ class TestConfigShow:
|
|
|
264
267
|
def test_config_with_vault(self, _):
|
|
265
268
|
result = config_show()
|
|
266
269
|
assert len(result.vaults) == 1
|
|
267
|
-
assert result.vaults[0].address ==
|
|
270
|
+
assert result.vaults[0].address == Web3.to_checksum_address(_SAVED_VAULT)
|
|
268
271
|
|
|
269
272
|
@patch(
|
|
270
273
|
"ipor_fusion.mcp.server.load_config",
|
|
@@ -319,7 +322,7 @@ class TestVaultList:
|
|
|
319
322
|
def test_with_vault(self, _):
|
|
320
323
|
result = vault_list()
|
|
321
324
|
assert len(result) == 1
|
|
322
|
-
assert result[0].address ==
|
|
325
|
+
assert result[0].address == Web3.to_checksum_address(_SAVED_VAULT)
|
|
323
326
|
assert result[0].label == "Test"
|
|
324
327
|
assert result[0].chain_id == 1
|
|
325
328
|
|
|
@@ -348,13 +351,23 @@ class TestVaultAdd:
|
|
|
348
351
|
with pytest.raises(ValueError):
|
|
349
352
|
vault_add(address="0xDEF", label="New Vault", chain_id=1)
|
|
350
353
|
|
|
354
|
+
@patch("ipor_fusion.mcp.server.save_config")
|
|
355
|
+
@patch(
|
|
356
|
+
"ipor_fusion.mcp.server.load_config",
|
|
357
|
+
return_value=_config_with_vault("0xABC"),
|
|
358
|
+
)
|
|
359
|
+
def test_a_malformed_saved_entry_is_not_updated(self, mock_load, mock_save):
|
|
360
|
+
with pytest.raises(ValueError):
|
|
361
|
+
vault_add(address="0xABC", label="Updated", chain_id=1)
|
|
362
|
+
mock_save.assert_not_called()
|
|
363
|
+
|
|
351
364
|
@patch("ipor_fusion.mcp.server.save_config")
|
|
352
365
|
@patch(
|
|
353
366
|
"ipor_fusion.mcp.server.load_config",
|
|
354
367
|
return_value=_config_with_vault(),
|
|
355
368
|
)
|
|
356
369
|
def test_update_existing(self, mock_load, mock_save):
|
|
357
|
-
result = vault_add(address=
|
|
370
|
+
result = vault_add(address=_SAVED_VAULT, label="Updated", chain_id=1)
|
|
358
371
|
assert "updated" in result.message.lower()
|
|
359
372
|
saved_cfg = mock_save.call_args[0][0]
|
|
360
373
|
assert len(saved_cfg.vaults) == 1
|
|
@@ -368,7 +381,7 @@ class TestVaultRemove:
|
|
|
368
381
|
return_value=_config_with_vault(),
|
|
369
382
|
)
|
|
370
383
|
def test_remove_existing(self, mock_load, mock_save):
|
|
371
|
-
result = vault_remove(address=
|
|
384
|
+
result = vault_remove(address=_SAVED_VAULT)
|
|
372
385
|
assert "removed" in result.message.lower()
|
|
373
386
|
saved_cfg = mock_save.call_args[0][0]
|
|
374
387
|
assert len(saved_cfg.vaults) == 0
|