ipor-fusion 4.0.0.dev57__tar.gz → 4.0.0.dev60__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.dev57 → ipor_fusion-4.0.0.dev60}/AGENTS.md +4 -1
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/PKG-INFO +1 -1
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/vault_info_view.py +4 -8
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/vault_data.py +25 -454
- ipor_fusion-4.0.0.dev60/src/ipor_fusion/vault_managers.py +185 -0
- ipor_fusion-4.0.0.dev60/src/ipor_fusion/vault_positions.py +238 -0
- ipor_fusion-4.0.0.dev60/src/ipor_fusion/vault_reads.py +81 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/conftest.py +4 -1
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_commands.py +4 -4
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_helpers.py +21 -22
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_pending_requests_scan.py +4 -7
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/MIGRATION_4.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/README.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cache.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/vault_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/vault_health.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/access.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/context.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/contract.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/multicall.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/discovery.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/simulation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/errors.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/explorer.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/mcp/models.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/mcp/server.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/morpho_api.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/types.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/validation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/src/ipor_fusion/vault_info.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/vault_info_golden/clean.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/vault_info_golden/clean.txt +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/vault_info_golden/rich.json +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/fixtures/vault_info_golden/rich.txt +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cache.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_market_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_vault_info_golden.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_vault_predates_getters.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_contract_call.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_ccip_indexer.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_ccip_quote.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_chainlink.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_lane.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_crosschain_transport.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_explorer.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_external_state_executor.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_live.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_outcome.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_readers.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_mcp_models.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_mcp_server.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_reader_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_validation.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_vault_simulator_blocks.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/tests/typed_client/consumer.py +0 -0
- {ipor_fusion-4.0.0.dev57 → ipor_fusion-4.0.0.dev60}/uv.lock +0 -0
|
@@ -184,7 +184,10 @@ the lifecycle matrix follows that.
|
|
|
184
184
|
balance-graph checks
|
|
185
185
|
- `vault_data.py` — public vault read: `fetch_vault_data` → `VaultData` (with `FeeData`,
|
|
186
186
|
`WithdrawManagerData`), `fetch_deployment_info`, cached `resolve_token_symbol` /
|
|
187
|
-
`resolve_token_decimals`; `
|
|
187
|
+
`resolve_token_decimals`; it composes `vault_reads.py` (the shared Multicall3 batch
|
|
188
|
+
and degrade-to-``None`` helpers), `vault_managers.py` (fee and WithdrawManager
|
|
189
|
+
snapshots) and `vault_positions.py` (Morpho/Aave position breakdowns and their token
|
|
190
|
+
prices); `vault_info.py` — `build_vault_info_json`, the
|
|
188
191
|
`fusion vault info --json` document (contract names via an injected `resolve_name`),
|
|
189
192
|
and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
|
|
190
193
|
`cache.py` the on-disk contract and deployment caches (`~/.cache/ipor-fusion/`).
|
|
@@ -32,16 +32,11 @@ from ipor_fusion.readers.hypercore import HyperCorePendingState
|
|
|
32
32
|
from ipor_fusion.substrates import (
|
|
33
33
|
format_market_label,
|
|
34
34
|
)
|
|
35
|
-
from ipor_fusion.types import ChainId, PlasmaVaultAddress
|
|
35
|
+
from ipor_fusion.types import ZERO_ADDRESS, ChainId, PlasmaVaultAddress
|
|
36
36
|
from ipor_fusion.validation import (
|
|
37
37
|
validate_vault_state,
|
|
38
38
|
)
|
|
39
|
-
from ipor_fusion.vault_data import
|
|
40
|
-
_ZERO_ADDRESS,
|
|
41
|
-
VaultData,
|
|
42
|
-
_safe_call,
|
|
43
|
-
fetch_deployment_info,
|
|
44
|
-
)
|
|
39
|
+
from ipor_fusion.vault_data import VaultData, fetch_deployment_info
|
|
45
40
|
from ipor_fusion.vault_info import (
|
|
46
41
|
BLOCK_EXPLORER_URLS,
|
|
47
42
|
IPOR_APP_URL,
|
|
@@ -53,6 +48,7 @@ from ipor_fusion.vault_info import (
|
|
|
53
48
|
_wad_to_usd,
|
|
54
49
|
build_vault_info_json,
|
|
55
50
|
)
|
|
51
|
+
from ipor_fusion.vault_reads import _safe_call
|
|
56
52
|
|
|
57
53
|
|
|
58
54
|
def _print_role_accounts_table(role_accounts: list[dict[str, Any]]) -> None:
|
|
@@ -275,7 +271,7 @@ def _print_fees(data: VaultData) -> None:
|
|
|
275
271
|
click.echo("Fees:")
|
|
276
272
|
click.echo(f" Fee Manager: {fees.fee_manager or 'N/A'}")
|
|
277
273
|
dao_payee = fees.ipor_dao_fee_recipient
|
|
278
|
-
if not dao_payee or dao_payee ==
|
|
274
|
+
if not dao_payee or dao_payee == ZERO_ADDRESS:
|
|
279
275
|
dao_payee = "N/A"
|
|
280
276
|
click.echo(f" IPOR DAO payee: {dao_payee}")
|
|
281
277
|
onboarding_fee = _format_fee_percent(_wad_to_percent(fees.deposit_fee_wad))
|
|
@@ -10,18 +10,14 @@ Token symbols, decimals and deployment blocks are cached on disk
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
-
from
|
|
14
|
-
from concurrent.futures import Future, ThreadPoolExecutor
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
15
14
|
from dataclasses import dataclass, field
|
|
16
15
|
from functools import partial
|
|
17
16
|
from typing import Any, TypeVar
|
|
18
17
|
|
|
19
|
-
import requests
|
|
20
|
-
from eth_abi.exceptions import DecodingError
|
|
21
18
|
from eth_typing import HexAddress
|
|
22
19
|
from eth_utils import function_signature_to_4byte_selector
|
|
23
20
|
from web3 import Web3
|
|
24
|
-
from web3.exceptions import ContractLogicError, TimeExhausted, Web3RPCError
|
|
25
21
|
from web3.types import ChecksumAddress
|
|
26
22
|
|
|
27
23
|
from ipor_fusion.cache import (
|
|
@@ -36,28 +32,16 @@ from ipor_fusion.core.access import AccessManager, RoleAccount, role_account_sor
|
|
|
36
32
|
from ipor_fusion.core.context import Web3Context
|
|
37
33
|
from ipor_fusion.core.contract import Call
|
|
38
34
|
from ipor_fusion.core.erc20 import ERC20
|
|
39
|
-
from ipor_fusion.core.
|
|
40
|
-
FeeAccount,
|
|
41
|
-
FeeManager,
|
|
42
|
-
HighWaterMarkPerformanceFee,
|
|
43
|
-
RecipientFee,
|
|
44
|
-
)
|
|
45
|
-
from ipor_fusion.core.multicall import Multicall3
|
|
46
|
-
from ipor_fusion.core.oracle import PriceOracleMiddleware, price_oracle_middleware
|
|
35
|
+
from ipor_fusion.core.oracle import price_oracle_middleware
|
|
47
36
|
from ipor_fusion.core.plasma_vault import BalanceFuse, PlasmaVault
|
|
48
|
-
from ipor_fusion.core.withdraw_manager import AccountRequest, WithdrawManager
|
|
49
37
|
from ipor_fusion.errors import (
|
|
50
38
|
EmptyCallResultError,
|
|
51
|
-
LogScanError,
|
|
52
39
|
UnsupportedVaultVersionError,
|
|
53
40
|
)
|
|
54
41
|
from ipor_fusion.explorer import get_deployment_tx
|
|
55
42
|
from ipor_fusion.market_ids import IporFusionMarkets
|
|
56
43
|
from ipor_fusion.readers.aave_v3 import (
|
|
57
|
-
AaveV3FuseReader,
|
|
58
|
-
AaveV3PoolAddressesProvider,
|
|
59
44
|
AaveV3PositionBreakdown,
|
|
60
|
-
AaveV3Reader,
|
|
61
45
|
)
|
|
62
46
|
from ipor_fusion.readers.hypercore import (
|
|
63
47
|
HYPEREVM_CHAIN_ID,
|
|
@@ -65,15 +49,11 @@ from ipor_fusion.readers.hypercore import (
|
|
|
65
49
|
read_hypercore_vault_state,
|
|
66
50
|
)
|
|
67
51
|
from ipor_fusion.readers.lending_health import (
|
|
68
|
-
AAVE_V3_MARKET_IDS,
|
|
69
|
-
MORPHO_MARKET_IDS,
|
|
70
52
|
VaultLendingHealth,
|
|
71
53
|
fetch_vault_lending_health,
|
|
72
54
|
)
|
|
73
55
|
from ipor_fusion.readers.morpho import (
|
|
74
56
|
MorphoPositionBreakdown,
|
|
75
|
-
MorphoReader,
|
|
76
|
-
morpho_blue_address,
|
|
77
57
|
)
|
|
78
58
|
from ipor_fusion.readers.unpriceable import (
|
|
79
59
|
MiddlewarePricedToken,
|
|
@@ -84,10 +64,30 @@ from ipor_fusion.types import (
|
|
|
84
64
|
ChainId,
|
|
85
65
|
FuseAddress,
|
|
86
66
|
MarketId,
|
|
87
|
-
MorphoBlueMarketId,
|
|
88
67
|
PlasmaVaultAddress,
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
)
|
|
69
|
+
from ipor_fusion.vault_managers import (
|
|
70
|
+
FeeData as FeeData,
|
|
71
|
+
)
|
|
72
|
+
from ipor_fusion.vault_managers import (
|
|
73
|
+
WithdrawManagerData as WithdrawManagerData,
|
|
74
|
+
)
|
|
75
|
+
from ipor_fusion.vault_managers import (
|
|
76
|
+
_fetch_fee_data,
|
|
77
|
+
_fetch_withdraw_manager,
|
|
78
|
+
)
|
|
79
|
+
from ipor_fusion.vault_positions import (
|
|
80
|
+
_collect_breakdown_token_addresses,
|
|
81
|
+
_fetch_aave_pools,
|
|
82
|
+
_fetch_aave_positions,
|
|
83
|
+
_fetch_breakdown_token_prices,
|
|
84
|
+
_fetch_morpho_positions,
|
|
85
|
+
)
|
|
86
|
+
from ipor_fusion.vault_reads import (
|
|
87
|
+
_ROLE_SCAN_ERRORS,
|
|
88
|
+
_read_batch,
|
|
89
|
+
_safe_call,
|
|
90
|
+
_unless_unimplemented,
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
T = TypeVar("T")
|
|
@@ -103,55 +103,6 @@ _logger = logging.getLogger(__name__)
|
|
|
103
103
|
_NO_CONTRACT = "no contract"
|
|
104
104
|
_NO_CHAIN = ChainId(0)
|
|
105
105
|
|
|
106
|
-
_ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@dataclass
|
|
110
|
-
class FeeData:
|
|
111
|
-
"""Vault fee configuration: vault-level fee data plus FeeManager state.
|
|
112
|
-
|
|
113
|
-
Every field is optional because each degrades independently: a vault may
|
|
114
|
-
have no fee account configured (no FeeManager at all), and FeeManagers
|
|
115
|
-
predating the deposit fee / high-water mark revert on those getters.
|
|
116
|
-
Scales differ per source, so every field names its own: `*_bps` is basis
|
|
117
|
-
points (10000 = 100%), `*_wad` is WAD (1e18 = 100%). Pair each with the
|
|
118
|
-
matching converter — `_bps_to_percent` / `_wad_to_percent` — and a
|
|
119
|
-
mismatch reads wrong at the call site.
|
|
120
|
-
|
|
121
|
-
`*_vault_bps` and `*_manager_bps` are the SAME fee read from two places:
|
|
122
|
-
the vault charges the former, the FeeManager believes it configured the
|
|
123
|
-
latter. They agree in a healthy deployment; a difference means the two
|
|
124
|
-
configurations have drifted.
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
performance_fee_vault_bps: int | None = None
|
|
128
|
-
management_fee_vault_bps: int | None = None
|
|
129
|
-
management_fee_last_update: int | None = None
|
|
130
|
-
unrealized_management_fee: int | None = None
|
|
131
|
-
fee_manager: ChecksumAddress | None = None
|
|
132
|
-
ipor_dao_fee_recipient: ChecksumAddress | None = None
|
|
133
|
-
deposit_fee_wad: int | None = None
|
|
134
|
-
performance_fee_manager_bps: int | None = None
|
|
135
|
-
management_fee_manager_bps: int | None = None
|
|
136
|
-
performance_fee_recipients: list[RecipientFee] | None = None
|
|
137
|
-
management_fee_recipients: list[RecipientFee] | None = None
|
|
138
|
-
high_water_mark: HighWaterMarkPerformanceFee | None = None
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@dataclass
|
|
142
|
-
class WithdrawManagerData:
|
|
143
|
-
"""On-chain state snapshot from the WithdrawManager contract."""
|
|
144
|
-
|
|
145
|
-
withdraw_window: int
|
|
146
|
-
# None (not 0) when the getter failed: a zero fee and an unreadable fee
|
|
147
|
-
# are different facts, and the fees section reports both.
|
|
148
|
-
request_fee: int | None
|
|
149
|
-
withdraw_fee: int | None
|
|
150
|
-
shares_to_release: int
|
|
151
|
-
last_release_funds_timestamp: int
|
|
152
|
-
# None when the WithdrawRequestUpdated scan or its reads failed.
|
|
153
|
-
pending_requests: list[AccountRequest] | None
|
|
154
|
-
|
|
155
106
|
|
|
156
107
|
@dataclass
|
|
157
108
|
class VaultData:
|
|
@@ -234,36 +185,6 @@ class VaultData:
|
|
|
234
185
|
unimplemented_getters: list[str] = field(default_factory=list)
|
|
235
186
|
|
|
236
187
|
|
|
237
|
-
# Failure modes of the event scans behind optional data (role holders, pending
|
|
238
|
-
# withdraw requests): a log scan the provider would not let complete
|
|
239
|
-
# (LogScanError), plus JSON-RPC and transport errors from the reads that follow.
|
|
240
|
-
_ROLE_SCAN_ERRORS = (
|
|
241
|
-
LogScanError,
|
|
242
|
-
ContractLogicError,
|
|
243
|
-
Web3RPCError,
|
|
244
|
-
TimeExhausted,
|
|
245
|
-
requests.RequestException,
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _scan_or_none(func: Callable[[], T]) -> T | None:
|
|
250
|
-
"""An event scan behind optional data; None (never a partial result) when
|
|
251
|
-
it cannot complete."""
|
|
252
|
-
try:
|
|
253
|
-
return func()
|
|
254
|
-
except _ROLE_SCAN_ERRORS as exc:
|
|
255
|
-
_logger.debug("scan unavailable: %s: %s", type(exc).__name__, exc)
|
|
256
|
-
return None
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _safe_call(func: Callable[[], T]) -> T | None:
|
|
260
|
-
try:
|
|
261
|
-
return func()
|
|
262
|
-
except (ContractLogicError, Web3RPCError, TimeExhausted) as exc:
|
|
263
|
-
_logger.debug("_safe_call suppressed %s: %s", type(exc).__name__, exc)
|
|
264
|
-
return None
|
|
265
|
-
|
|
266
|
-
|
|
267
188
|
_MARKET_ID_SELECTOR = function_signature_to_4byte_selector("MARKET_ID()")
|
|
268
189
|
|
|
269
190
|
|
|
@@ -285,18 +206,6 @@ def _fuse_market_id_call(ctx: Web3Context, fuse: ChecksumAddress) -> Call[int]:
|
|
|
285
206
|
_UNCAPPED = 2**256 - 1
|
|
286
207
|
|
|
287
208
|
|
|
288
|
-
def _unless_unimplemented(value: T | None, call: Call[T]) -> T | None:
|
|
289
|
-
"""An optional batch read, re-run when it failed: ``None`` only when the
|
|
290
|
-
contract returned no data (it predates the getter); a revert or RPC error
|
|
291
|
-
raises exactly what `Call.call()` would, so real failures stay loud."""
|
|
292
|
-
if value is not None:
|
|
293
|
-
return value
|
|
294
|
-
try:
|
|
295
|
-
return call.call()
|
|
296
|
-
except EmptyCallResultError:
|
|
297
|
-
return None
|
|
298
|
-
|
|
299
|
-
|
|
300
209
|
def _missing_required_getter(
|
|
301
210
|
plasma_vault: PlasmaVault, exc: EmptyCallResultError
|
|
302
211
|
) -> UnsupportedVaultVersionError:
|
|
@@ -308,24 +217,6 @@ def _missing_required_getter(
|
|
|
308
217
|
)
|
|
309
218
|
|
|
310
219
|
|
|
311
|
-
def _read_batch(
|
|
312
|
-
ctx: Web3Context,
|
|
313
|
-
required: Sequence[Call[Any]],
|
|
314
|
-
optional: Sequence[Call[Any]] = (),
|
|
315
|
-
) -> tuple[list[Any], list[Any]]:
|
|
316
|
-
"""Read `required` + `optional` in one Multicall3 round trip.
|
|
317
|
-
|
|
318
|
-
A failing optional read is ``None``. A failing required read is re-run on
|
|
319
|
-
its own, so it raises exactly what `Call.call()` would.
|
|
320
|
-
"""
|
|
321
|
-
results = Multicall3(ctx).try_aggregate([*required, *optional])
|
|
322
|
-
head = [
|
|
323
|
-
value if value is not None else call.call()
|
|
324
|
-
for value, call in zip(results, required, strict=False)
|
|
325
|
-
]
|
|
326
|
-
return head, results[len(required) :]
|
|
327
|
-
|
|
328
|
-
|
|
329
220
|
def resolve_token_symbol(ctx: Web3Context, address: HexAddress) -> str:
|
|
330
221
|
"""ERC-20 symbol, cached; ``"no contract"`` when nothing is deployed at
|
|
331
222
|
``address`` and ``""`` when the symbol cannot be read."""
|
|
@@ -369,326 +260,6 @@ def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> int | None:
|
|
|
369
260
|
return decimals
|
|
370
261
|
|
|
371
262
|
|
|
372
|
-
def _fetch_withdraw_manager(
|
|
373
|
-
ctx: Web3Context, plasma_vault: PlasmaVault
|
|
374
|
-
) -> tuple[ChecksumAddress | None, WithdrawManagerData | None]:
|
|
375
|
-
"""Resolve the vault's WithdrawManager and snapshot its state."""
|
|
376
|
-
address = plasma_vault.withdraw_manager_address()
|
|
377
|
-
if not address:
|
|
378
|
-
return None, None
|
|
379
|
-
wm = WithdrawManager(ctx, address)
|
|
380
|
-
_, (window, request_fee, withdraw_fee, shares, last_ts) = _read_batch(
|
|
381
|
-
ctx,
|
|
382
|
-
[],
|
|
383
|
-
[
|
|
384
|
-
wm.get_withdraw_window(),
|
|
385
|
-
wm.get_request_fee(),
|
|
386
|
-
wm.get_withdraw_fee(),
|
|
387
|
-
wm.get_shares_to_release(),
|
|
388
|
-
wm.get_last_release_funds_timestamp(),
|
|
389
|
-
],
|
|
390
|
-
)
|
|
391
|
-
return address, WithdrawManagerData(
|
|
392
|
-
withdraw_window=window or 0,
|
|
393
|
-
request_fee=request_fee,
|
|
394
|
-
withdraw_fee=withdraw_fee,
|
|
395
|
-
shares_to_release=shares or 0,
|
|
396
|
-
last_release_funds_timestamp=last_ts or 0,
|
|
397
|
-
pending_requests=_scan_or_none(wm.get_pending_requests),
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def _fetch_fee_manager_address(
|
|
402
|
-
ctx: Web3Context, fee_account: ChecksumAddress
|
|
403
|
-
) -> ChecksumAddress | None:
|
|
404
|
-
"""Resolve FeeAccount.FEE_MANAGER(); None when it is not a FeeAccount.
|
|
405
|
-
|
|
406
|
-
The fee account reported by a vault is not guaranteed to be a FeeAccount
|
|
407
|
-
contract — older deployments can name a plain recipient address. An EOA
|
|
408
|
-
answers eth_call with empty data, which fails ABI decoding instead of
|
|
409
|
-
reverting, so `_safe_call` alone does not cover it.
|
|
410
|
-
"""
|
|
411
|
-
try:
|
|
412
|
-
return _safe_call(FeeAccount(ctx, fee_account).fee_manager().call)
|
|
413
|
-
except DecodingError:
|
|
414
|
-
return None
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _fetch_fee_data(ctx: Web3Context, plasma_vault: PlasmaVault) -> FeeData:
|
|
418
|
-
"""Fetch the vault's fee configuration, including the FeeManager hop.
|
|
419
|
-
|
|
420
|
-
Three dependent round trips: vault-level fee data, then
|
|
421
|
-
FeeAccount.FEE_MANAGER() to discover the FeeManager, then the FeeManager
|
|
422
|
-
getters. Nothing raises on a revert — every read degrades to ``None``, so
|
|
423
|
-
vaults without a fee account and FeeManagers predating the deposit fee or
|
|
424
|
-
high-water mark still produce a usable snapshot.
|
|
425
|
-
"""
|
|
426
|
-
_, (perf, mgmt, unrealized) = _read_batch(
|
|
427
|
-
ctx,
|
|
428
|
-
[],
|
|
429
|
-
[
|
|
430
|
-
plasma_vault.get_performance_fee_data(),
|
|
431
|
-
plasma_vault.get_management_fee_data(),
|
|
432
|
-
plasma_vault.get_unrealized_management_fee(),
|
|
433
|
-
],
|
|
434
|
-
)
|
|
435
|
-
result = FeeData(
|
|
436
|
-
performance_fee_vault_bps=perf.fee_in_percentage if perf else None,
|
|
437
|
-
management_fee_vault_bps=mgmt.fee_in_percentage if mgmt else None,
|
|
438
|
-
management_fee_last_update=mgmt.last_update_timestamp if mgmt else None,
|
|
439
|
-
unrealized_management_fee=unrealized,
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
# A FeeManager deploys two separate escrow accounts (performance and
|
|
443
|
-
# management) but is itself the FEE_MANAGER of both, so either hop
|
|
444
|
-
# resolves to the same address; take whichever is configured.
|
|
445
|
-
fee_account = next(
|
|
446
|
-
(
|
|
447
|
-
data.fee_account
|
|
448
|
-
for data in (perf, mgmt)
|
|
449
|
-
if data is not None and data.fee_account != _ZERO_ADDRESS
|
|
450
|
-
),
|
|
451
|
-
None,
|
|
452
|
-
)
|
|
453
|
-
if fee_account is None:
|
|
454
|
-
return result
|
|
455
|
-
|
|
456
|
-
fee_manager_addr = _fetch_fee_manager_address(ctx, fee_account)
|
|
457
|
-
if not fee_manager_addr or fee_manager_addr == _ZERO_ADDRESS:
|
|
458
|
-
return result
|
|
459
|
-
result.fee_manager = fee_manager_addr
|
|
460
|
-
|
|
461
|
-
fee_manager = FeeManager(ctx, fee_manager_addr)
|
|
462
|
-
(
|
|
463
|
-
_,
|
|
464
|
-
(
|
|
465
|
-
result.deposit_fee_wad,
|
|
466
|
-
result.performance_fee_manager_bps,
|
|
467
|
-
result.management_fee_manager_bps,
|
|
468
|
-
result.performance_fee_recipients,
|
|
469
|
-
result.management_fee_recipients,
|
|
470
|
-
result.ipor_dao_fee_recipient,
|
|
471
|
-
result.high_water_mark,
|
|
472
|
-
),
|
|
473
|
-
) = _read_batch(
|
|
474
|
-
ctx,
|
|
475
|
-
[],
|
|
476
|
-
[
|
|
477
|
-
fee_manager.get_deposit_fee(),
|
|
478
|
-
fee_manager.get_total_performance_fee(),
|
|
479
|
-
fee_manager.get_total_management_fee(),
|
|
480
|
-
fee_manager.get_performance_fee_recipients(),
|
|
481
|
-
fee_manager.get_management_fee_recipients(),
|
|
482
|
-
fee_manager.get_ipor_dao_fee_recipient_address(),
|
|
483
|
-
fee_manager.get_plasma_vault_high_water_mark_performance_fee(),
|
|
484
|
-
],
|
|
485
|
-
)
|
|
486
|
-
return result
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def _collect_morpho_substrates(
|
|
490
|
-
market_substrates: dict[MarketId, list[bytes]],
|
|
491
|
-
) -> dict[MarketId, list[MorphoBlueMarketId]]:
|
|
492
|
-
"""Filter + parse Morpho Blue market IDs from raw substrate bytes."""
|
|
493
|
-
result: dict[MarketId, list[MorphoBlueMarketId]] = {}
|
|
494
|
-
for mid, subs in market_substrates.items():
|
|
495
|
-
if mid not in MORPHO_MARKET_IDS:
|
|
496
|
-
continue
|
|
497
|
-
morpho_ids = [parse_morpho_market_id(sub) for sub in subs if len(sub) == 32]
|
|
498
|
-
if morpho_ids:
|
|
499
|
-
result[mid] = morpho_ids
|
|
500
|
-
return result
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
def _fetch_morpho_positions(
|
|
504
|
-
ctx: Web3Context,
|
|
505
|
-
pool: ThreadPoolExecutor,
|
|
506
|
-
vault_addr: ChecksumAddress,
|
|
507
|
-
market_substrates: dict[MarketId, list[bytes]],
|
|
508
|
-
) -> dict[MarketId, list[MorphoPositionBreakdown]] | None:
|
|
509
|
-
"""Fetch collateral / borrow / supply breakdown for every Morpho substrate.
|
|
510
|
-
|
|
511
|
-
Each IPOR Morpho market (MORPHO_MARKET_IDS) can hold multiple morpho
|
|
512
|
-
market_id substrates; each substrate has an independent position. The
|
|
513
|
-
on-chain balance fuse reports a single netted number per IPOR market —
|
|
514
|
-
this helper exposes the three-way decomposition behind that number.
|
|
515
|
-
"""
|
|
516
|
-
per_market_substrates = _collect_morpho_substrates(market_substrates)
|
|
517
|
-
if not per_market_substrates:
|
|
518
|
-
return None
|
|
519
|
-
|
|
520
|
-
reader = MorphoReader(ctx, morpho_blue_address(ctx.chain_id))
|
|
521
|
-
futures: dict[MarketId, list[Future]] = {
|
|
522
|
-
mid: [
|
|
523
|
-
pool.submit(
|
|
524
|
-
_safe_call,
|
|
525
|
-
lambda mid_hex=morpho_mid: reader.position_breakdown(
|
|
526
|
-
mid_hex, vault_addr
|
|
527
|
-
),
|
|
528
|
-
)
|
|
529
|
-
for morpho_mid in morpho_mids
|
|
530
|
-
]
|
|
531
|
-
for mid, morpho_mids in per_market_substrates.items()
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
result: dict[MarketId, list[MorphoPositionBreakdown]] = {}
|
|
535
|
-
for mid, per_market in futures.items():
|
|
536
|
-
breakdowns = [bd for fut in per_market if (bd := fut.result()) is not None]
|
|
537
|
-
if breakdowns:
|
|
538
|
-
result[mid] = breakdowns
|
|
539
|
-
return result or None
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def _collect_aave_substrate_assets(
|
|
543
|
-
market_substrates: dict[MarketId, list[bytes]],
|
|
544
|
-
) -> dict[MarketId, list[ChecksumAddress]]:
|
|
545
|
-
"""Filter + parse asset addresses from Aave V3 substrate bytes.
|
|
546
|
-
|
|
547
|
-
Aave substrates are zero-padded plain addresses (12 zero bytes + 20 address
|
|
548
|
-
bytes). Returns a per-market list of checksummed asset addresses.
|
|
549
|
-
"""
|
|
550
|
-
result: dict[MarketId, list[ChecksumAddress]] = {}
|
|
551
|
-
for mid, subs in market_substrates.items():
|
|
552
|
-
if mid not in AAVE_V3_MARKET_IDS:
|
|
553
|
-
continue
|
|
554
|
-
assets: list[ChecksumAddress] = []
|
|
555
|
-
for sub in subs:
|
|
556
|
-
hex_str = sub.hex()
|
|
557
|
-
if len(hex_str) != 64:
|
|
558
|
-
continue
|
|
559
|
-
assets.append(Web3.to_checksum_address("0x" + hex_str[24:]))
|
|
560
|
-
if assets:
|
|
561
|
-
result[mid] = assets
|
|
562
|
-
return result
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
def _fetch_aave_pools(
|
|
566
|
-
ctx: Web3Context, balance_fuses: list[BalanceFuse]
|
|
567
|
-
) -> dict[MarketId, ChecksumAddress]:
|
|
568
|
-
"""Map every Aave V3 market to the Pool its balance fuse reads.
|
|
569
|
-
|
|
570
|
-
Aave V3 Core, Aave V3 Prime and SparkLend share the fuse contracts but not
|
|
571
|
-
the Pool, so the Pool is resolved per market rather than per chain: the
|
|
572
|
-
fuse's PoolAddressesProvider, then its Pool, one batch each. A balance
|
|
573
|
-
fuse that is not an Aave V3 fuse reverts or answers with empty data and is
|
|
574
|
-
left out.
|
|
575
|
-
"""
|
|
576
|
-
fuses = {
|
|
577
|
-
bf.market_id: bf.fuse
|
|
578
|
-
for bf in balance_fuses
|
|
579
|
-
if bf.market_id in AAVE_V3_MARKET_IDS
|
|
580
|
-
}
|
|
581
|
-
if not fuses:
|
|
582
|
-
return {}
|
|
583
|
-
multicall = Multicall3(ctx)
|
|
584
|
-
providers = multicall.try_aggregate(
|
|
585
|
-
[
|
|
586
|
-
AaveV3FuseReader(ctx, fuse).pool_addresses_provider()
|
|
587
|
-
for fuse in fuses.values()
|
|
588
|
-
]
|
|
589
|
-
)
|
|
590
|
-
resolved = {
|
|
591
|
-
mid: provider
|
|
592
|
-
for mid, provider in zip(fuses, providers, strict=True)
|
|
593
|
-
if provider is not None
|
|
594
|
-
}
|
|
595
|
-
pools = multicall.try_aggregate(
|
|
596
|
-
[AaveV3PoolAddressesProvider(ctx, p).get_pool() for p in resolved.values()]
|
|
597
|
-
)
|
|
598
|
-
return {
|
|
599
|
-
mid: pool for mid, pool in zip(resolved, pools, strict=True) if pool is not None
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
def _fetch_aave_positions(
|
|
604
|
-
ctx: Web3Context,
|
|
605
|
-
pool: ThreadPoolExecutor,
|
|
606
|
-
vault_addr: ChecksumAddress,
|
|
607
|
-
aave_pools: dict[MarketId, ChecksumAddress],
|
|
608
|
-
market_substrates: dict[MarketId, list[bytes]],
|
|
609
|
-
) -> dict[MarketId, list[AaveV3PositionBreakdown]] | None:
|
|
610
|
-
"""Fetch supply / variable / stable debt per Aave V3 asset substrate.
|
|
611
|
-
|
|
612
|
-
The on-chain `getUserAccountData` aggregates all reserves into a single
|
|
613
|
-
base-currency total — this helper exposes the per-asset decomposition.
|
|
614
|
-
Each market is read from its own Pool (`aave_pools`); markets without one
|
|
615
|
-
are skipped. Empty positions (vault allows the asset but holds none of it)
|
|
616
|
-
are dropped.
|
|
617
|
-
"""
|
|
618
|
-
per_market_assets = {
|
|
619
|
-
mid: assets
|
|
620
|
-
for mid, assets in _collect_aave_substrate_assets(market_substrates).items()
|
|
621
|
-
if mid in aave_pools
|
|
622
|
-
}
|
|
623
|
-
if not per_market_assets:
|
|
624
|
-
return None
|
|
625
|
-
|
|
626
|
-
readers = {mid: AaveV3Reader(ctx, aave_pools[mid]) for mid in per_market_assets}
|
|
627
|
-
futures: dict[MarketId, list[Future]] = {
|
|
628
|
-
mid: [
|
|
629
|
-
pool.submit(
|
|
630
|
-
_safe_call, partial(readers[mid].position_breakdown, asset, vault_addr)
|
|
631
|
-
)
|
|
632
|
-
for asset in assets
|
|
633
|
-
]
|
|
634
|
-
for mid, assets in per_market_assets.items()
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
result: dict[MarketId, list[AaveV3PositionBreakdown]] = {}
|
|
638
|
-
for mid, per_market in futures.items():
|
|
639
|
-
breakdowns = [
|
|
640
|
-
bd
|
|
641
|
-
for fut in per_market
|
|
642
|
-
if (bd := fut.result()) is not None and not bd.is_empty
|
|
643
|
-
]
|
|
644
|
-
if breakdowns:
|
|
645
|
-
result[mid] = breakdowns
|
|
646
|
-
return result or None
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
def _collect_breakdown_token_addresses(
|
|
650
|
-
morpho_positions: dict[MarketId, list[MorphoPositionBreakdown]] | None,
|
|
651
|
-
aave_positions: dict[MarketId, list[AaveV3PositionBreakdown]] | None,
|
|
652
|
-
) -> set[ChecksumAddress]:
|
|
653
|
-
"""Return the set of unique token addresses appearing in any breakdown.
|
|
654
|
-
|
|
655
|
-
Used to batch-fetch USD prices via the price oracle in one parallel pass.
|
|
656
|
-
"""
|
|
657
|
-
addrs: set[ChecksumAddress] = set()
|
|
658
|
-
for morpho_list in (morpho_positions or {}).values():
|
|
659
|
-
for pb in morpho_list:
|
|
660
|
-
addrs.add(pb.loan_token)
|
|
661
|
-
addrs.add(pb.collateral_token)
|
|
662
|
-
for aave_list in (aave_positions or {}).values():
|
|
663
|
-
for ab in aave_list:
|
|
664
|
-
addrs.add(ab.asset)
|
|
665
|
-
return addrs
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
def _fetch_breakdown_token_prices(
|
|
669
|
-
ctx: Web3Context,
|
|
670
|
-
oracle: PriceOracleMiddleware,
|
|
671
|
-
addresses: set[ChecksumAddress],
|
|
672
|
-
) -> dict[HexAddress, float] | None:
|
|
673
|
-
"""Fetch USD prices for breakdown tokens in one batch via the vault's oracle.
|
|
674
|
-
|
|
675
|
-
Returns a dict keyed by lowercase token address. Tokens with no oracle
|
|
676
|
-
source configured are simply omitted (callers treat absence as "no price").
|
|
677
|
-
"""
|
|
678
|
-
if not addresses:
|
|
679
|
-
return None
|
|
680
|
-
ordered = sorted(addresses)
|
|
681
|
-
prices = Multicall3(ctx).try_aggregate(
|
|
682
|
-
[oracle.get_asset_price(addr) for addr in ordered]
|
|
683
|
-
)
|
|
684
|
-
result = {
|
|
685
|
-
parse_hex_address(addr): price.readable()
|
|
686
|
-
for addr, price in zip(ordered, prices, strict=True)
|
|
687
|
-
if price is not None
|
|
688
|
-
}
|
|
689
|
-
return result or None
|
|
690
|
-
|
|
691
|
-
|
|
692
263
|
def _fetch_block(ctx: Web3Context, block_number: int | None) -> tuple[int, int]:
|
|
693
264
|
"""``(block_number, timestamp)`` of the requested block, latest by default."""
|
|
694
265
|
resolved = ctx.web3.eth.block_number if block_number is None else block_number
|