ipor-fusion 4.0.0.dev78__tar.gz → 4.0.0.dev86__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.dev78 → ipor_fusion-4.0.0.dev86}/MIGRATION_4.md +86 -2
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/PKG-INFO +1 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/pyproject.toml +1 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/__init__.py +12 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/access.py +9 -4
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/contract.py +2 -2
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/external_state_executor.py +66 -37
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/multicall.py +1 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/simulation.py +16 -9
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/chainlink.py +5 -4
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/indexer.py +53 -16
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/quote.py +7 -4
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/discovery.py +31 -11
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/simulation.py +6 -4
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/errors.py +84 -13
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/external_state_events.py +4 -2
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/server.py +1 -4
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/morpho_api.py +82 -60
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/hypercore.py +19 -8
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/hypercore_outcome.py +4 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_managers.py +5 -3
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_multicall.py +2 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_helpers.py +13 -5
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_market_morpho_blue.py +90 -2
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_contract_call.py +23 -5
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_indexer.py +52 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_quote.py +6 -3
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_chainlink.py +2 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_lane.py +40 -7
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_transport.py +5 -4
- ipor_fusion-4.0.0.dev86/tests/test_error_hierarchy.py +118 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_external_state_executor.py +32 -28
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_outcome.py +12 -1
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_readers.py +17 -3
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_server.py +1 -5
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_public_api.py +6 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_simulator_blocks.py +15 -5
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.claude-plugin/marketplace.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.claude-plugin/plugin.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.env.example +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.gemini/settings.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.git-blame-ignore-revs +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/cd.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/ci.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/commit-lint.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/notify-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/python-build.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/python-publish.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/release.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/report-slack.yml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.gitignore +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.mcp.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.pre-commit-config.yaml +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.python-version +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/AGENTS.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CHANGELOG.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CLAUDE.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CODEOWNERS +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/LICENSE +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/README.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/build.sh +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/CONTRIBUTING.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/README.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/advanced_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/external_state_margin_leg_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/simple_aave_v3_supply_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/pytest.ini +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/scripts/commit-check.sh +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/scripts/crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/skills/ipor-deploy-vault/SKILL.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/about.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cache.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/chains.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/config_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/config_store.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/main.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/market_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/options.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_cmd.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_health.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_info_view.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_rendering.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/config/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/config/roles.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/address.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/context.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/erc20.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fuse_action.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fusion_factory.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/logs.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/oracle.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/errors.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/lane.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/logs.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/messages.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/transport.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/dependency_graph.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/entry_cli.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/entry_mcp.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/explorer.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/field_docs.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/async_action.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/erc4626.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/external_state.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/merkl.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/universal.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/architecture.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/glossary.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/invariants.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/quickstart.md +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/market_ids.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/market_info.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/guide.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/models.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/py.typed +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/__init__.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/aave_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/aave_v4.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/compound_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/morpho.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/position_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/ramses_v2.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/unpriceable.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/substrates.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/types.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/validation.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_data.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_info.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_positions.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_reads.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/UniswapV3UniversalRouter.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain_recovery.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_euler_v2.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_foundry.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_guide.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_hypercore_model.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_hypercore_shadow.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_simulate.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_validation.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/addresses.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/conftest.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/constants.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/hypercore_hip3_flow.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/hypercore_market55.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/clean.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/clean.txt +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/rich.json +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/rich.txt +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_about.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_access_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cache.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_chains.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_changelog.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_commands.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_config_store.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_lending_health.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_pending_requests_scan.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_role_accounts.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_info_golden.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_predates_getters.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_contract_encoder.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_contracts.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_errors.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_layering.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_readiness.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_readiness_report.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_erc20.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_composed_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_euler_looping_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_vaults.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_explorer.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_external_state_events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fee_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fuse_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fusion_instance.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_guide.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_deployment.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_flow.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_live.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_simulation.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_logs.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_models.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_vault_validate.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_multicall.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_oracle.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_oracle_mapping.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_plasma_vault.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_reader_encoding.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_readiness_selection.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_revert_decoding.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_rewards_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_roles.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_sdk_layering.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_claim_morpho_rewards.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_canary.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_hypercore.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_hyperevm.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_lifecycle.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_batch_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_arbitrum.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_crosschain.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_external_state_executor_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_looping_aave_v3_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_morpho_blue.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v3.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v5_ramses.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_readers_integration.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_release_funds.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_swap_on_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_vault_from_scratch_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_solidity_compiler.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_solidity_mirrors.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_substrates.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_types_and_events.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_unpriceable_tokens.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_validation.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_dep_graph.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_info_onchain_base.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_web3_context.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_withdraw_manager.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/typed_client/consumer.py +0 -0
- {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/uv.lock +0 -0
|
@@ -220,8 +220,10 @@ except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
|
|
|
220
220
|
- It raises `MorphoApiError` for a market id or address it parses (market and V1
|
|
221
221
|
allocation ids, V1 flow-cap ids, vault/asset/owner/token/oracle/IRM/allocator
|
|
222
222
|
addresses) when the value is invalid, or absent where the field is optional in the
|
|
223
|
-
answer.
|
|
224
|
-
without `address
|
|
223
|
+
answer. Any other answer it cannot read (a missing key such as a `loanAsset`
|
|
224
|
+
without `address`, a value of the wrong type, a body that is not a JSON object) and
|
|
225
|
+
any transport failure also raise `MorphoApiError`, never `KeyError`, `TypeError` or
|
|
226
|
+
`ValueError` (see [Errors](#errors)). When matching a market's own allocation and flow cap, an id that
|
|
225
227
|
does not parse matches nothing, and a V1 flow cap without an id is skipped, as
|
|
226
228
|
before. `--market` rejects a malformed id.
|
|
227
229
|
- `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
|
|
@@ -232,3 +234,85 @@ except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
|
|
|
232
234
|
- The on-disk caches (`~/.cache/ipor-fusion/`) key addresses lowercased; older entries
|
|
233
235
|
are rekeyed on load, so a contract name cached for one spelling now also resolves
|
|
234
236
|
for the other.
|
|
237
|
+
|
|
238
|
+
## Errors
|
|
239
|
+
|
|
240
|
+
Every exception class the SDK defines now derives from `IporFusionError` and is
|
|
241
|
+
importable from `ipor_fusion`; the builtin bases they had are kept. One rule holds
|
|
242
|
+
across the whole SDK: a caller's mistake (a bad argument, or an operation the object
|
|
243
|
+
was not built for) raises a plain `ValueError` (`TypeError` for an argument of the
|
|
244
|
+
wrong type, `KeyError` for an unknown name in a mapping-style lookup such as
|
|
245
|
+
`ipor_fusion.guide.guide_text`); a chain, provider or remote API answering something the
|
|
246
|
+
SDK cannot use raises an `IporFusionError` subclass. Except for
|
|
247
|
+
`CrosschainDiscoveryError` (discovery) and `SimulationError` (`quote_ccip_native_fee`),
|
|
248
|
+
every subclass that replaces a `ValueError` raise also derives from `ValueError`, so
|
|
249
|
+
`except ValueError` keeps catching it; catch the subclass to tell a chain fact from a
|
|
250
|
+
bad argument. Receipts,
|
|
251
|
+
logs and transports passed in that do not contain what the call needs (a clone
|
|
252
|
+
receipt without its events, logs without the lane's `BalanceProposed`, a CCIP
|
|
253
|
+
message to a chain the transport does not configure) stay `ValueError`: the input
|
|
254
|
+
is the wrong one.
|
|
255
|
+
|
|
256
|
+
- `MorphoApiError` (still a `RuntimeError`), `ExternalStateEventDecodeError` (still a
|
|
257
|
+
`ValueError`) and `VerifierResultUnavailable` (still a `LookupError`) now derive
|
|
258
|
+
from `IporFusionError`, so an `except IporFusionError` placed before their own
|
|
259
|
+
handlers catches them first. `MorphoApiError` lives in `ipor_fusion.errors` and is
|
|
260
|
+
exported from `ipor_fusion`; `ipor_fusion.morpho_api.MorphoApiError` still works.
|
|
261
|
+
- `EmptyCallResultError` is no longer an eth_abi `InsufficientDataBytes` (nor a
|
|
262
|
+
`DecodingError`). Code that caught either around `.call()` to tolerate a missing
|
|
263
|
+
contract or getter must catch `EmptyCallResultError`; return data that is present
|
|
264
|
+
but too short still raises eth_abi's error.
|
|
265
|
+
- Crosschain discovery (`discover_deployment`, `discover_lane_fuses`,
|
|
266
|
+
`detect_transport`, `open_lane`, `CrosschainDeployment.executor`) raises the new
|
|
267
|
+
`CrosschainDiscoveryError` instead of `ValueError` for on-chain configuration it
|
|
268
|
+
cannot use: a malformed or unknown market substrate, an executor managed by another
|
|
269
|
+
vault or of no known transport, a missing or duplicate lane fuse, or not exactly one
|
|
270
|
+
executor of a transport. An executor reporting `transportKind()` `UNDEFINED` is now
|
|
271
|
+
rejected by `detect_transport` instead of being returned. A transport kind passed in
|
|
272
|
+
that has no lane is still a `ValueError`.
|
|
273
|
+
- `Call.call()` and `Call.decode()` on a write-only `Call` raise `ValueError`, not
|
|
274
|
+
`RuntimeError`. `CrosschainSimulator.relay` raises `ValueError`, not
|
|
275
|
+
`RuntimeError`, for a message to a chain that was not added.
|
|
276
|
+
- `VaultSimulator.run` (provider error from `eth_simulateV1`, or a result count that
|
|
277
|
+
does not match the queued calls) and `CrosschainSimulator.relay` (not settled within
|
|
278
|
+
`max_rounds`) raise the new `SimulationRunError`, still a `RuntimeError`.
|
|
279
|
+
- The Morpho API client raises `MorphoApiError` for every response it cannot read and
|
|
280
|
+
for any `OSError` or `http.client.HTTPException` while fetching (it caught only
|
|
281
|
+
`URLError` and `TimeoutError` before).
|
|
282
|
+
- The MCP `market_meta_morpho` tool lets `MorphoApiError` propagate instead of
|
|
283
|
+
re-raising it as `ValueError`. MCP clients see the same tool error message; only a
|
|
284
|
+
direct Python call of the tool function sees the new type.
|
|
285
|
+
- `read_hypercore_nav` (and `read_hypercore_vault_state`, which calls it) raises the new
|
|
286
|
+
`HyperCoreValuationError` (still a `ValueError`) for a failed `spotBalance`,
|
|
287
|
+
`tokenInfo` or `accountMarginSummary` precompile read, a granted spot token the
|
|
288
|
+
oracle does not price, a price overflow and an unsupported HIP-3 dex bitmap;
|
|
289
|
+
`HyperCoreNav.balance_wad` raises it for a negative NAV.
|
|
290
|
+
- `read_hypercore_transfer_snapshot` raises `UnsupportedChainError` (still a
|
|
291
|
+
`ValueError`) on a chain with no known `HyperCorePendingReader` when none is passed.
|
|
292
|
+
- `fetch_verifier_result` no longer leaks `KeyError`, `TypeError` or `ValueError` from
|
|
293
|
+
an indexer answer it cannot read (not JSON, a missing or mistyped field, invalid hex
|
|
294
|
+
or address, an empty or non-array CCV list, or a result for a different message id,
|
|
295
|
+
both now checked): that
|
|
296
|
+
indexer is skipped like an unreachable one, and when none serves a readable result
|
|
297
|
+
it raises `VerifierResultUnavailable` naming each indexer's reason. A `message_id`
|
|
298
|
+
that is not 32 bytes is still a `ValueError`.
|
|
299
|
+
- `erc20_balance_slot` raises the new `BalanceSlotNotFoundError` (still a
|
|
300
|
+
`ValueError`) when no slot up to `max_slot` answers as the balances mapping; a
|
|
301
|
+
negative `max_slot` is a `ValueError` with its own message.
|
|
302
|
+
- The external-state market raises the new `ExternalStateError` (still a
|
|
303
|
+
`ValueError`): `read_external_state` for a storage word longer than 32 bytes, an
|
|
304
|
+
executor word with bits above the address or a pause word other than 0 or 1;
|
|
305
|
+
`ExternalStateExecutor.for_vault` when no executor is deployed and, instead of
|
|
306
|
+
eth_abi's `NonEmptyPaddingBytes`, for a slot that is not an address;
|
|
307
|
+
`parse_balance_proposed` / `find_balance_proposed` for one of the executor's own
|
|
308
|
+
`BalanceProposed` logs whose data is missing or malformed (including data that is
|
|
309
|
+
not hex at all, which raised `TypeError`) or whose hash is not the hash of its own
|
|
310
|
+
fields; and `mark_nav` when the propose receipt it fetched has no such log or
|
|
311
|
+
records another value or proposer. A log that is not the event or not from the
|
|
312
|
+
executor, and `find_balance_proposed` over logs that hold no match, are still
|
|
313
|
+
`ValueError`.
|
|
314
|
+
- `ccip_token_lane` raises the new `UnsupportedContractVersionError` (still a
|
|
315
|
+
`ValueError`) for an OnRamp version whose layout is not verified.
|
|
316
|
+
- `quote_ccip_native_fee` raises `SimulationError` (label `"quote"`, with
|
|
317
|
+
`revert_reason`) instead of `ValueError` when the call reverts before the CCIP fee
|
|
318
|
+
check. A call that succeeds without sending a message is still a `ValueError`.
|
|
@@ -173,16 +173,22 @@ from ipor_fusion.dependency_graph import (
|
|
|
173
173
|
)
|
|
174
174
|
from ipor_fusion.errors import (
|
|
175
175
|
CUSTOM_ERRORS,
|
|
176
|
+
BalanceSlotNotFoundError,
|
|
176
177
|
ContractNotFoundError,
|
|
178
|
+
CrosschainDiscoveryError,
|
|
177
179
|
EmptyCallResultError,
|
|
180
|
+
ExternalStateError,
|
|
181
|
+
HyperCoreValuationError,
|
|
178
182
|
IporFusionError,
|
|
179
183
|
LogScanError,
|
|
180
184
|
LogSourceUnavailable,
|
|
181
185
|
MorphoMarketNotFoundError,
|
|
182
186
|
NotPlasmaVaultError,
|
|
183
187
|
SimulationError,
|
|
188
|
+
SimulationRunError,
|
|
184
189
|
TransactionError,
|
|
185
190
|
UnsupportedChainError,
|
|
191
|
+
UnsupportedContractVersionError,
|
|
186
192
|
UnsupportedVaultVersionError,
|
|
187
193
|
decode_custom_error,
|
|
188
194
|
get_revert_reason,
|
|
@@ -715,8 +721,12 @@ __all__ = [
|
|
|
715
721
|
"format_market_label",
|
|
716
722
|
"market_name",
|
|
717
723
|
"DOCS",
|
|
724
|
+
"BalanceSlotNotFoundError",
|
|
718
725
|
"ContractNotFoundError",
|
|
726
|
+
"CrosschainDiscoveryError",
|
|
719
727
|
"EmptyCallResultError",
|
|
728
|
+
"ExternalStateError",
|
|
729
|
+
"HyperCoreValuationError",
|
|
720
730
|
"IporFusionError",
|
|
721
731
|
"LogScanError",
|
|
722
732
|
"LogSourceUnavailable",
|
|
@@ -724,7 +734,9 @@ __all__ = [
|
|
|
724
734
|
"NotPlasmaVaultError",
|
|
725
735
|
"TransactionError",
|
|
726
736
|
"SimulationError",
|
|
737
|
+
"SimulationRunError",
|
|
727
738
|
"UnsupportedChainError",
|
|
739
|
+
"UnsupportedContractVersionError",
|
|
728
740
|
"UnsupportedVaultVersionError",
|
|
729
741
|
"CUSTOM_ERRORS",
|
|
730
742
|
"decode_custom_error",
|
|
@@ -15,7 +15,11 @@ from ipor_fusion.core.context import Web3Context
|
|
|
15
15
|
from ipor_fusion.core.contract import Call, ContractWrapper
|
|
16
16
|
from ipor_fusion.core.multicall import Multicall3
|
|
17
17
|
from ipor_fusion.core.plasma_vault import PlasmaVault
|
|
18
|
-
from ipor_fusion.errors import
|
|
18
|
+
from ipor_fusion.errors import (
|
|
19
|
+
ContractNotFoundError,
|
|
20
|
+
EmptyCallResultError,
|
|
21
|
+
NotPlasmaVaultError,
|
|
22
|
+
)
|
|
19
23
|
from ipor_fusion.types import AccessManagerAddress, Period, PlasmaVaultAddress, RoleId
|
|
20
24
|
|
|
21
25
|
|
|
@@ -189,8 +193,9 @@ def resolve_access_manager(
|
|
|
189
193
|
|
|
190
194
|
Raises ContractNotFoundError when no code is deployed at the address (as
|
|
191
195
|
of ctx.default_block), and NotPlasmaVaultError when the contract does not
|
|
192
|
-
expose getAccessManagerAddress()
|
|
193
|
-
(
|
|
196
|
+
expose getAccessManagerAddress(): an empty eth_call return
|
|
197
|
+
(EmptyCallResultError), return data too short to decode
|
|
198
|
+
(InsufficientDataBytes) or a revert (ContractLogicError).
|
|
194
199
|
Provider/transport errors propagate unchanged.
|
|
195
200
|
"""
|
|
196
201
|
checksum = Web3.to_checksum_address(vault_address)
|
|
@@ -210,7 +215,7 @@ def resolve_access_manager(
|
|
|
210
215
|
.get_access_manager_address()
|
|
211
216
|
.call()
|
|
212
217
|
)
|
|
213
|
-
except (InsufficientDataBytes, ContractLogicError) as exc:
|
|
218
|
+
except (EmptyCallResultError, InsufficientDataBytes, ContractLogicError) as exc:
|
|
214
219
|
raise NotPlasmaVaultError(
|
|
215
220
|
f"Address {checksum} on chain {ctx.chain_id} does not appear "
|
|
216
221
|
f"to be a Plasma Vault."
|
|
@@ -73,7 +73,7 @@ class Call(Generic[T]):
|
|
|
73
73
|
"""Execute as `eth_call`; decode and convert per `output_types`/`decoder`."""
|
|
74
74
|
actual = self._resolve_ctx(ctx)
|
|
75
75
|
if not self.output_types:
|
|
76
|
-
raise
|
|
76
|
+
raise ValueError(
|
|
77
77
|
"Call.call() on a write-only Call — use .send() instead "
|
|
78
78
|
"(no output_types declared)."
|
|
79
79
|
)
|
|
@@ -91,7 +91,7 @@ class Call(Generic[T]):
|
|
|
91
91
|
"""Decode raw return data per `output_types`/`decoder` — the second half
|
|
92
92
|
of `.call()`, for return data fetched elsewhere (e.g. `Multicall3`)."""
|
|
93
93
|
if not self.output_types:
|
|
94
|
-
raise
|
|
94
|
+
raise ValueError("Call.decode() on a write-only Call.")
|
|
95
95
|
values = tuple(decode(self.output_types, bytes(raw)))
|
|
96
96
|
single: Any = values[0] if len(values) == 1 else values
|
|
97
97
|
if self.decoder is not None:
|
{ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/external_state_executor.py
RENAMED
|
@@ -31,6 +31,7 @@ from web3.types import BlockIdentifier, TxReceipt
|
|
|
31
31
|
|
|
32
32
|
from ipor_fusion.core.context import Web3Context
|
|
33
33
|
from ipor_fusion.core.contract import Call, ContractWrapper
|
|
34
|
+
from ipor_fusion.errors import ExternalStateError
|
|
34
35
|
from ipor_fusion.market_ids import IporFusionMarkets
|
|
35
36
|
from ipor_fusion.types import (
|
|
36
37
|
ZERO_ADDRESS,
|
|
@@ -107,6 +108,17 @@ def _hash_of(
|
|
|
107
108
|
)
|
|
108
109
|
|
|
109
110
|
|
|
111
|
+
def _no_balance_proposed(
|
|
112
|
+
executor: ChecksumAddress, balance_account: ChecksumAddress | None
|
|
113
|
+
) -> str:
|
|
114
|
+
for_account = (
|
|
115
|
+
f" for account {Web3.to_checksum_address(balance_account)}"
|
|
116
|
+
if balance_account is not None
|
|
117
|
+
else ""
|
|
118
|
+
)
|
|
119
|
+
return f"no BalanceProposed log from executor {executor}{for_account}"
|
|
120
|
+
|
|
121
|
+
|
|
110
122
|
def _require_matching_hash(
|
|
111
123
|
proposal: BalanceProposal, *, executor: ChecksumAddress, chain_id: ChainId
|
|
112
124
|
) -> None:
|
|
@@ -122,7 +134,7 @@ def _require_matching_hash(
|
|
|
122
134
|
if proposal.proposal_hash != _hash_of(
|
|
123
135
|
proposal, executor=executor, chain_id=chain_id
|
|
124
136
|
):
|
|
125
|
-
raise
|
|
137
|
+
raise ExternalStateError(
|
|
126
138
|
"proposal hash does not match its own fields: it pairs this value "
|
|
127
139
|
"with another proposal's hash, or it was computed for a different "
|
|
128
140
|
"executor or chain id"
|
|
@@ -191,7 +203,9 @@ def _storage_word(raw: bytes) -> int:
|
|
|
191
203
|
never a value an EVM slot can hold."""
|
|
192
204
|
data = bytes(raw)
|
|
193
205
|
if len(data) > 32:
|
|
194
|
-
raise
|
|
206
|
+
raise ExternalStateError(
|
|
207
|
+
f"storage word of {len(data)} bytes, more than a 32-byte slot"
|
|
208
|
+
)
|
|
195
209
|
return int.from_bytes(data.rjust(32, b"\x00"), "big")
|
|
196
210
|
|
|
197
211
|
|
|
@@ -230,9 +244,10 @@ def read_external_state(
|
|
|
230
244
|
hash by identity; ``pending`` is rejected), so the four values never mix
|
|
231
245
|
blocks; the block number is returned so executor reads can be pinned to
|
|
232
246
|
it. Decoding is canonical: a word longer than 32 bytes, an executor word
|
|
233
|
-
with bits above the address, or a pause word other than 0 or 1, raises
|
|
234
|
-
a plausible value; a zero
|
|
235
|
-
|
|
247
|
+
with bits above the address, or a pause word other than 0 or 1, raises
|
|
248
|
+
``ExternalStateError`` rather than passing as a plausible value; a zero
|
|
249
|
+
executor word is ``None`` (no executor deployed), which is a state, while
|
|
250
|
+
an RPC or decoding failure raises.
|
|
236
251
|
"""
|
|
237
252
|
vault = Web3.to_checksum_address(vault_address)
|
|
238
253
|
read_at, block = _snapshot_block(ctx)
|
|
@@ -241,11 +256,11 @@ def read_external_state(
|
|
|
241
256
|
_storage_word(ctx.get_storage_at(vault, base + i, read_at)) for i in range(4)
|
|
242
257
|
]
|
|
243
258
|
if words[0] >> 160:
|
|
244
|
-
raise
|
|
259
|
+
raise ExternalStateError(
|
|
245
260
|
f"malformed executor word in the external-state storage of {vault}"
|
|
246
261
|
)
|
|
247
262
|
if words[3] not in (0, 1):
|
|
248
|
-
raise
|
|
263
|
+
raise ExternalStateError(
|
|
249
264
|
f"malformed pause flag in the external-state storage of {vault}"
|
|
250
265
|
)
|
|
251
266
|
return ExternalStateVaultState(
|
|
@@ -309,24 +324,26 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
309
324
|
) -> ExternalStateExecutor:
|
|
310
325
|
"""Resolve the per-vault executor from vault storage and wrap it.
|
|
311
326
|
|
|
312
|
-
Raises `
|
|
313
|
-
zero) rather than returning a wrapper around the zero address, and
|
|
314
|
-
|
|
315
|
-
corrupt slot
|
|
327
|
+
Raises `ExternalStateError` when no executor is deployed yet (the slot
|
|
328
|
+
reads zero) rather than returning a wrapper around the zero address, and
|
|
329
|
+
when the slot is not an address (its high 12 bytes are non-zero: a wrong
|
|
330
|
+
or corrupt slot) or is longer than 32 bytes.
|
|
316
331
|
"""
|
|
317
|
-
|
|
318
|
-
# address is the slot's low 20 bytes, `decode` verifies the rest is zero.
|
|
319
|
-
raw = bytes(
|
|
332
|
+
word = _storage_word(
|
|
320
333
|
ctx.get_storage_at(
|
|
321
334
|
Web3.to_checksum_address(vault_address), cls._EXECUTOR_STORAGE_SLOT
|
|
322
335
|
)
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
336
|
+
)
|
|
337
|
+
if word >> 160:
|
|
338
|
+
raise ExternalStateError(
|
|
339
|
+
f"malformed executor word in the external-state storage of "
|
|
340
|
+
f"{vault_address}"
|
|
341
|
+
)
|
|
342
|
+
if word == 0:
|
|
343
|
+
raise ExternalStateError(
|
|
327
344
|
f"no ExternalStateExecutor deployed for vault {vault_address}"
|
|
328
345
|
)
|
|
329
|
-
return cls(ctx,
|
|
346
|
+
return cls(ctx, Web3.to_checksum_address(f"0x{word:040x}"))
|
|
330
347
|
|
|
331
348
|
def balances(self, balance_account: ChecksumAddress) -> Call[Amount]:
|
|
332
349
|
"""Underlying-unit balance the executor tracks for `balance_account`.
|
|
@@ -634,11 +651,10 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
634
651
|
Accepts receipt-shaped logs (HexBytes) and JSON-RPC / `eth_simulateV1`
|
|
635
652
|
-shaped logs (`0x`-hex strings) alike, so it also decodes rows from an
|
|
636
653
|
event index. Needs no chain access. Raises `ValueError` for a log that
|
|
637
|
-
is not this event
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
first.
|
|
654
|
+
is not this event or was not emitted by this executor, and for a call
|
|
655
|
+
with no chain id available at all; `ExternalStateError` (also a
|
|
656
|
+
`ValueError`) for one of ours that carries a hash that is not the hash
|
|
657
|
+
of its own fields, or whose data is missing or malformed.
|
|
642
658
|
|
|
643
659
|
Security: two checks, and both are needed. This executor must have
|
|
644
660
|
emitted the log, and `proposal_hash` must be the hash of the log's own
|
|
@@ -682,7 +698,7 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
682
698
|
"""Decode a log whose topic0 is already known to be this event."""
|
|
683
699
|
data = log.get("data")
|
|
684
700
|
if data is None:
|
|
685
|
-
raise
|
|
701
|
+
raise ExternalStateError("BalanceProposed log carries no data")
|
|
686
702
|
try:
|
|
687
703
|
# Unpacked in the order `BalanceProposed` declares them; all six
|
|
688
704
|
# are unindexed, so topics carries topic0 alone.
|
|
@@ -690,8 +706,8 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
690
706
|
cls._BALANCE_PROPOSED_TYPES,
|
|
691
707
|
HexBytes(data),
|
|
692
708
|
)
|
|
693
|
-
except (DecodingError, ValueError) as exc:
|
|
694
|
-
raise
|
|
709
|
+
except (DecodingError, ValueError, TypeError) as exc:
|
|
710
|
+
raise ExternalStateError(f"malformed BalanceProposed data: {exc}") from exc
|
|
695
711
|
return BalanceProposal(
|
|
696
712
|
balance_account=Web3.to_checksum_address(account),
|
|
697
713
|
proposer=Web3.to_checksum_address(proposer),
|
|
@@ -750,7 +766,19 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
750
766
|
silently skips, and over a range you filtered yourself a foreign log
|
|
751
767
|
means the filter is wrong, not that the row is uninteresting.
|
|
752
768
|
"""
|
|
753
|
-
|
|
769
|
+
found = self._last_balance_proposed(logs, balance_account, chain_id)
|
|
770
|
+
if found is not None:
|
|
771
|
+
return found
|
|
772
|
+
raise ValueError(_no_balance_proposed(self._address, balance_account))
|
|
773
|
+
|
|
774
|
+
def _last_balance_proposed(
|
|
775
|
+
self,
|
|
776
|
+
logs: Iterable[RawLog],
|
|
777
|
+
balance_account: ChecksumAddress | None,
|
|
778
|
+
chain_id: ChainId | None,
|
|
779
|
+
) -> BalanceProposal | None:
|
|
780
|
+
"""`find_balance_proposed` without the raise when nothing matches."""
|
|
781
|
+
self._require_address()
|
|
754
782
|
wanted = (
|
|
755
783
|
Web3.to_checksum_address(balance_account)
|
|
756
784
|
if balance_account is not None
|
|
@@ -773,11 +801,7 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
773
801
|
executor=self._address,
|
|
774
802
|
chain_id=self._require_chain_id(chain_id),
|
|
775
803
|
)
|
|
776
|
-
|
|
777
|
-
for_account = f" for account {wanted}" if wanted is not None else ""
|
|
778
|
-
raise ValueError(
|
|
779
|
-
f"no BalanceProposed log from executor {expected_emitter}{for_account}"
|
|
780
|
-
)
|
|
804
|
+
return found
|
|
781
805
|
|
|
782
806
|
def mark_nav(
|
|
783
807
|
self,
|
|
@@ -840,19 +864,24 @@ class ExternalStateExecutor(ContractWrapper):
|
|
|
840
864
|
)
|
|
841
865
|
# Verified against the CONFIRMER's chain, which is the one the confirm
|
|
842
866
|
# will be checked on; the wrapper's own ctx may be a third context.
|
|
843
|
-
proposal = self.
|
|
844
|
-
propose_receipt["logs"], balance_account,
|
|
867
|
+
proposal = self._last_balance_proposed(
|
|
868
|
+
propose_receipt["logs"], balance_account, confirmer_ctx.chain_id
|
|
845
869
|
)
|
|
870
|
+
if proposal is None:
|
|
871
|
+
raise ExternalStateError(
|
|
872
|
+
"the propose receipt records "
|
|
873
|
+
+ _no_balance_proposed(self._address, balance_account)
|
|
874
|
+
)
|
|
846
875
|
# The receipt is data from the node, so confirm only what we asked for:
|
|
847
876
|
# signing off on a proposal that differs from the one just sent would
|
|
848
877
|
# attest a value this call never chose.
|
|
849
878
|
if proposal.value != value:
|
|
850
|
-
raise
|
|
879
|
+
raise ExternalStateError(
|
|
851
880
|
f"proposed {value} but the receipt records {proposal.value}; "
|
|
852
881
|
"refusing to confirm"
|
|
853
882
|
)
|
|
854
883
|
if proposal.proposer != proposer_address:
|
|
855
|
-
raise
|
|
884
|
+
raise ExternalStateError(
|
|
856
885
|
f"proposal was made by {proposal.proposer}, not {proposer_address}; "
|
|
857
886
|
"refusing to confirm"
|
|
858
887
|
)
|
|
@@ -23,7 +23,7 @@ MULTICALL3_ADDRESS: ChecksumAddress = Web3.to_checksum_address(
|
|
|
23
23
|
DEFAULT_BATCH_SIZE = 100
|
|
24
24
|
|
|
25
25
|
# A sub-call that reverts, returns nothing or returns undecodable data.
|
|
26
|
-
_CALL_FAILURES = (ContractLogicError, DecodingError)
|
|
26
|
+
_CALL_FAILURES = (ContractLogicError, EmptyCallResultError, DecodingError)
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class Multicall3(ContractWrapper):
|
|
@@ -15,7 +15,12 @@ from web3.utils.address import get_create_address
|
|
|
15
15
|
|
|
16
16
|
from ipor_fusion.core.contract import Call, _encode_calldata
|
|
17
17
|
from ipor_fusion.core.fuse_action import FuseAction
|
|
18
|
-
from ipor_fusion.errors import
|
|
18
|
+
from ipor_fusion.errors import (
|
|
19
|
+
BalanceSlotNotFoundError,
|
|
20
|
+
SimulationError,
|
|
21
|
+
SimulationRunError,
|
|
22
|
+
decode_custom_error,
|
|
23
|
+
)
|
|
19
24
|
from ipor_fusion.types import PlasmaVaultAddress
|
|
20
25
|
|
|
21
26
|
#: ``n`` (the sent block's index in the run) -> account overrides for it.
|
|
@@ -570,7 +575,7 @@ class VaultSimulator:
|
|
|
570
575
|
)
|
|
571
576
|
if "error" in response:
|
|
572
577
|
err = response["error"]
|
|
573
|
-
raise
|
|
578
|
+
raise SimulationRunError(f"eth_simulateV1 failed: {err}")
|
|
574
579
|
|
|
575
580
|
return self._parse_response(response["result"])
|
|
576
581
|
|
|
@@ -597,7 +602,7 @@ class VaultSimulator:
|
|
|
597
602
|
for block_result in result:
|
|
598
603
|
raw_calls.extend(block_result.get("calls", []))
|
|
599
604
|
if len(raw_calls) != len(sources):
|
|
600
|
-
raise
|
|
605
|
+
raise SimulationRunError(
|
|
601
606
|
"eth_simulateV1 returned "
|
|
602
607
|
f"{len(raw_calls)} call results for {len(sources)} queued calls"
|
|
603
608
|
)
|
|
@@ -747,13 +752,13 @@ def erc20_balance_slot(
|
|
|
747
752
|
"""Find the storage slot of ``token``'s ``balances`` mapping by overriding
|
|
748
753
|
every candidate in one ``eth_simulateV1`` request, using a distinct holder
|
|
749
754
|
per slot, until ``balanceOf`` reflects the override (slot 9 for Circle's
|
|
750
|
-
FiatToken, 0 for OpenZeppelin ERC20). Raises ``
|
|
751
|
-
to ``max_slot`` answers, which is the case for tokens whose
|
|
752
|
-
a plain address-keyed mapping
|
|
755
|
+
FiatToken, 0 for OpenZeppelin ERC20). Raises ``BalanceSlotNotFoundError``
|
|
756
|
+
when no slot up to ``max_slot`` answers, which is the case for tokens whose
|
|
757
|
+
balances are not a plain address-keyed mapping, and ``ValueError`` for a
|
|
758
|
+
negative ``max_slot``."""
|
|
753
759
|
probe = 0x1234_5678_9ABC
|
|
754
|
-
no_slot = f"no balances mapping slot found for {token} up to {max_slot}"
|
|
755
760
|
if max_slot < 0:
|
|
756
|
-
raise ValueError(
|
|
761
|
+
raise ValueError(f"max_slot must be non-negative, got {max_slot}")
|
|
757
762
|
|
|
758
763
|
sim = VaultSimulator(web3, block=block)
|
|
759
764
|
for slot in range(max_slot + 1):
|
|
@@ -774,7 +779,9 @@ def erc20_balance_slot(
|
|
|
774
779
|
for slot in range(max_slot + 1):
|
|
775
780
|
if result.get(f"balance_slot_{slot}") == probe:
|
|
776
781
|
return slot
|
|
777
|
-
raise
|
|
782
|
+
raise BalanceSlotNotFoundError(
|
|
783
|
+
f"no balances mapping slot found for {token} up to {max_slot}"
|
|
784
|
+
)
|
|
778
785
|
|
|
779
786
|
|
|
780
787
|
def _normalize_call_error(
|
{ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/chainlink.py
RENAMED
|
@@ -16,6 +16,7 @@ from eth_typing import ChecksumAddress
|
|
|
16
16
|
from ipor_fusion.core.context import Web3Context
|
|
17
17
|
from ipor_fusion.core.contract import Call, ContractWrapper
|
|
18
18
|
from ipor_fusion.crosschain.contracts import _address
|
|
19
|
+
from ipor_fusion.errors import UnsupportedContractVersionError
|
|
19
20
|
from ipor_fusion.types import ZERO_ADDRESS, MessageId
|
|
20
21
|
|
|
21
22
|
|
|
@@ -118,9 +119,9 @@ def ccip_token_lane(
|
|
|
118
119
|
chain_selector: int,
|
|
119
120
|
) -> CcipTokenLane:
|
|
120
121
|
"""Read, on ``router``'s chain, whether a message lane and a ``token`` lane
|
|
121
|
-
exist towards ``chain_selector``. Seven reads at most. Raises
|
|
122
|
-
before reading static config when the
|
|
123
|
-
layout."""
|
|
122
|
+
exist towards ``chain_selector``. Seven reads at most. Raises
|
|
123
|
+
``UnsupportedContractVersionError`` before reading static config when the
|
|
124
|
+
OnRamp version has an unverified layout."""
|
|
124
125
|
router_wrapper = CcipRouter(ctx, router)
|
|
125
126
|
if not router_wrapper.is_chain_supported(chain_selector).call():
|
|
126
127
|
return CcipTokenLane(chain_selector, message_lane=False)
|
|
@@ -128,7 +129,7 @@ def ccip_token_lane(
|
|
|
128
129
|
on_ramp_wrapper = CcipOnRamp(ctx, on_ramp)
|
|
129
130
|
on_ramp_version = on_ramp_wrapper.type_and_version().call()
|
|
130
131
|
if on_ramp_version not in _SUPPORTED_ON_RAMP_VERSIONS:
|
|
131
|
-
raise
|
|
132
|
+
raise UnsupportedContractVersionError(
|
|
132
133
|
f"unsupported CCIP OnRamp version {on_ramp_version!r} at {on_ramp}; "
|
|
133
134
|
"ccip_token_lane supports OnRamp 2.0.0"
|
|
134
135
|
)
|
{ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/indexer.py
RENAMED
|
@@ -21,6 +21,7 @@ from web3 import Web3
|
|
|
21
21
|
|
|
22
22
|
from ipor_fusion.core.contract import Call
|
|
23
23
|
from ipor_fusion.crosschain.ccip.chainlink import CcipOffRamp
|
|
24
|
+
from ipor_fusion.errors import IporFusionError
|
|
24
25
|
from ipor_fusion.types import MessageId
|
|
25
26
|
|
|
26
27
|
#: Chainlink Labs' public mainnet indexers, tried in order.
|
|
@@ -32,9 +33,11 @@ INDEXER_URLS: tuple[str, ...] = (
|
|
|
32
33
|
_HEADERS = {"Accept": "application/json", "User-Agent": "ipor-fusion-sdk"}
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
class VerifierResultUnavailable(LookupError):
|
|
36
|
+
class VerifierResultUnavailable(IporFusionError, LookupError):
|
|
36
37
|
"""No indexer could serve a verifier result for the message (not verified
|
|
37
|
-
yet,
|
|
38
|
+
yet, the indexers were unreachable, or they answered something unreadable);
|
|
39
|
+
the message says which, per indexer. Also a LookupError: the result looked
|
|
40
|
+
up is not there (yet)."""
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
@dataclass(frozen=True, slots=True)
|
|
@@ -61,7 +64,10 @@ def fetch_verifier_result(
|
|
|
61
64
|
timeout: float = 15.0,
|
|
62
65
|
) -> VerifierResult:
|
|
63
66
|
"""``GET /v1/verifierresults/<messageId>`` from the first indexer that has
|
|
64
|
-
a result; raises :class:`VerifierResultUnavailable` otherwise.
|
|
67
|
+
a readable result; raises :class:`VerifierResultUnavailable` otherwise. An
|
|
68
|
+
indexer whose answer cannot be read (not JSON, a missing or mistyped field,
|
|
69
|
+
invalid hex or address, a result for another message) is skipped like one
|
|
70
|
+
that is unreachable."""
|
|
65
71
|
if len(message_id) != 32:
|
|
66
72
|
raise ValueError("message_id must be 32 bytes")
|
|
67
73
|
hex_id = "0x" + message_id.hex()
|
|
@@ -77,26 +83,57 @@ def fetch_verifier_result(
|
|
|
77
83
|
if response.status_code != 200:
|
|
78
84
|
reasons.append(f"{base}: HTTP {response.status_code}")
|
|
79
85
|
continue
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
try:
|
|
87
|
+
result = _parse_answer(response.json(), message_id, base)
|
|
88
|
+
except (KeyError, IndexError, TypeError, ValueError) as exc:
|
|
89
|
+
reasons.append(f"{base}: malformed answer ({type(exc).__name__}: {exc})")
|
|
90
|
+
continue
|
|
91
|
+
if result is None:
|
|
82
92
|
reasons.append(f"{base}: no result yet")
|
|
83
93
|
continue
|
|
84
|
-
result
|
|
85
|
-
return VerifierResult(
|
|
86
|
-
message_id=MessageId(bytes.fromhex(result["message_id"][2:])),
|
|
87
|
-
ccv_addresses=tuple(
|
|
88
|
-
Web3.to_checksum_address(a) for a in result["message_ccv_addresses"]
|
|
89
|
-
),
|
|
90
|
-
ccv_data=bytes.fromhex(result["ccv_data"][2:]),
|
|
91
|
-
executor=Web3.to_checksum_address(result["message_executor_address"]),
|
|
92
|
-
message=result["message"],
|
|
93
|
-
indexer=base,
|
|
94
|
-
)
|
|
94
|
+
return result
|
|
95
95
|
raise VerifierResultUnavailable(
|
|
96
96
|
f"no verifier result for {hex_id}: " + "; ".join(reasons)
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _hex_bytes(value: Any) -> bytes:
|
|
101
|
+
if not isinstance(value, str) or not value.startswith("0x"):
|
|
102
|
+
raise ValueError(f"not 0x-prefixed hex: {value!r}")
|
|
103
|
+
return bytes.fromhex(value[2:])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parse_answer(
|
|
107
|
+
body: Any, message_id: MessageId, indexer: str
|
|
108
|
+
) -> VerifierResult | None:
|
|
109
|
+
"""The first result of an indexer's JSON answer; ``None`` when it has none.
|
|
110
|
+
Anything else it cannot read raises a builtin error, reported by the caller
|
|
111
|
+
as a malformed answer."""
|
|
112
|
+
if not isinstance(body, Mapping):
|
|
113
|
+
raise TypeError("answer is not a JSON object")
|
|
114
|
+
results = body.get("results")
|
|
115
|
+
if not results:
|
|
116
|
+
return None
|
|
117
|
+
result = results[0]["verifierResult"]
|
|
118
|
+
served_id = _hex_bytes(result["message_id"])
|
|
119
|
+
if served_id != message_id:
|
|
120
|
+
raise ValueError(f"result is for message 0x{served_id.hex()}")
|
|
121
|
+
message = result["message"]
|
|
122
|
+
if not isinstance(message, Mapping):
|
|
123
|
+
raise TypeError("message is not an object")
|
|
124
|
+
ccvs = result["message_ccv_addresses"]
|
|
125
|
+
if not isinstance(ccvs, list) or not ccvs:
|
|
126
|
+
raise ValueError("message_ccv_addresses is not a non-empty array")
|
|
127
|
+
return VerifierResult(
|
|
128
|
+
message_id=MessageId(served_id),
|
|
129
|
+
ccv_addresses=tuple(Web3.to_checksum_address(a) for a in ccvs),
|
|
130
|
+
ccv_data=_hex_bytes(result["ccv_data"]),
|
|
131
|
+
executor=Web3.to_checksum_address(result["message_executor_address"]),
|
|
132
|
+
message=message,
|
|
133
|
+
indexer=indexer,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
100
137
|
def manual_execution(
|
|
101
138
|
off_ramp: CcipOffRamp,
|
|
102
139
|
encoded_message: bytes,
|