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.
Files changed (294) hide show
  1. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/MIGRATION_4.md +86 -2
  2. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/PKG-INFO +1 -1
  3. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/pyproject.toml +1 -1
  4. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/__init__.py +12 -0
  5. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/access.py +9 -4
  6. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/contract.py +2 -2
  7. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/external_state_executor.py +66 -37
  8. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/multicall.py +1 -1
  9. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/simulation.py +16 -9
  10. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/chainlink.py +5 -4
  11. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/indexer.py +53 -16
  12. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/quote.py +7 -4
  13. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/discovery.py +31 -11
  14. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/simulation.py +6 -4
  15. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/errors.py +84 -13
  16. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/external_state_events.py +4 -2
  17. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/server.py +1 -4
  18. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/morpho_api.py +82 -60
  19. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/hypercore.py +19 -8
  20. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/hypercore_outcome.py +4 -1
  21. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_managers.py +5 -3
  22. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_multicall.py +2 -1
  23. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_helpers.py +13 -5
  24. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_market_morpho_blue.py +90 -2
  25. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_contract_call.py +23 -5
  26. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_indexer.py +52 -0
  27. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_quote.py +6 -3
  28. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_chainlink.py +2 -1
  29. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_lane.py +40 -7
  30. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_transport.py +5 -4
  31. ipor_fusion-4.0.0.dev86/tests/test_error_hierarchy.py +118 -0
  32. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_external_state_executor.py +32 -28
  33. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_outcome.py +12 -1
  34. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_readers.py +17 -3
  35. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_server.py +1 -5
  36. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_public_api.py +6 -0
  37. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_simulator_blocks.py +15 -5
  38. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.claude-plugin/marketplace.json +0 -0
  39. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.claude-plugin/plugin.json +0 -0
  40. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.env.example +0 -0
  41. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.gemini/settings.json +0 -0
  42. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.git-blame-ignore-revs +0 -0
  43. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/cd.yml +0 -0
  44. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/ci.yml +0 -0
  45. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/commit-lint.yml +0 -0
  46. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/notify-slack.yml +0 -0
  47. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/python-build.yml +0 -0
  48. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/python-publish.yml +0 -0
  49. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/release.yml +0 -0
  50. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.github/workflows/report-slack.yml +0 -0
  51. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.gitignore +0 -0
  52. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.mcp.json +0 -0
  53. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.pre-commit-config.yaml +0 -0
  54. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/.python-version +0 -0
  55. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/AGENTS.md +0 -0
  56. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CHANGELOG.md +0 -0
  57. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CLAUDE.md +0 -0
  58. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/CODEOWNERS +0 -0
  59. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/LICENSE +0 -0
  60. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/README.md +0 -0
  61. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/build.sh +0 -0
  62. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/CONTRIBUTING.md +0 -0
  63. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/README.md +0 -0
  64. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  65. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  66. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  67. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  68. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/external_state_margin_leg_base.py +0 -0
  69. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/examples/simple_aave_v3_supply_base.py +0 -0
  70. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/pytest.ini +0 -0
  71. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/scripts/commit-check.sh +0 -0
  72. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/scripts/crosschain_readiness.py +0 -0
  73. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/skills/ipor-deploy-vault/SKILL.md +0 -0
  74. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/about.py +0 -0
  75. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cache.py +0 -0
  76. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/chains.py +0 -0
  77. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/__init__.py +0 -0
  78. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  79. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/config_cmd.py +0 -0
  80. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/config_store.py +0 -0
  81. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/main.py +0 -0
  82. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/market_cmd.py +0 -0
  83. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/options.py +0 -0
  84. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  85. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  86. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_health.py +0 -0
  87. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  88. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  89. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  90. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/config/__init__.py +0 -0
  91. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/config/roles.py +0 -0
  92. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/__init__.py +0 -0
  93. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/address.py +0 -0
  94. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/context.py +0 -0
  95. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/erc20.py +0 -0
  96. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fee_manager.py +0 -0
  97. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fuse_action.py +0 -0
  98. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/fusion_factory.py +0 -0
  99. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/logs.py +0 -0
  100. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/oracle.py +0 -0
  101. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/plasma_vault.py +0 -0
  102. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/rewards_manager.py +0 -0
  103. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  104. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/__init__.py +0 -0
  105. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  106. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  107. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  108. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  109. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  110. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  111. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/contracts.py +0 -0
  112. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/errors.py +0 -0
  113. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/lane.py +0 -0
  114. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/logs.py +0 -0
  115. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/messages.py +0 -0
  116. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  117. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  118. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  119. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  120. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  121. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/crosschain/transport.py +0 -0
  122. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/dependency_graph.py +0 -0
  123. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/entry_cli.py +0 -0
  124. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/entry_mcp.py +0 -0
  125. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/explorer.py +0 -0
  126. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/field_docs.py +0 -0
  127. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/__init__.py +0 -0
  128. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  129. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/async_action.py +0 -0
  130. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/base.py +0 -0
  131. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  132. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  133. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  134. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  135. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  136. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/erc4626.py +0 -0
  137. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  138. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/events.py +0 -0
  139. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/external_state.py +0 -0
  140. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  141. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  142. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/hypercore.py +0 -0
  143. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  144. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/merkl.py +0 -0
  145. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/morpho.py +0 -0
  146. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  147. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  148. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/fuses/universal.py +0 -0
  149. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/__init__.py +0 -0
  150. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/architecture.md +0 -0
  151. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/glossary.md +0 -0
  152. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/invariants.md +0 -0
  153. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/guide/quickstart.md +0 -0
  154. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/market_ids.py +0 -0
  155. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/market_info.py +0 -0
  156. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/__init__.py +0 -0
  157. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/guide.py +0 -0
  158. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/mcp/models.py +0 -0
  159. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/py.typed +0 -0
  160. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/__init__.py +0 -0
  161. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/aave_v3.py +0 -0
  162. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/aave_v4.py +0 -0
  163. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/compound_v3.py +0 -0
  164. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/lending_health.py +0 -0
  165. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/morpho.py +0 -0
  166. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  167. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  168. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  169. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/position_manager.py +0 -0
  170. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  171. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  172. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/readers/unpriceable.py +0 -0
  173. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/substrates.py +0 -0
  174. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/types.py +0 -0
  175. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/validation.py +0 -0
  176. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_data.py +0 -0
  177. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_info.py +0 -0
  178. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_positions.py +0 -0
  179. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/src/ipor_fusion/vault_reads.py +0 -0
  180. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/UniswapV3UniversalRouter.py +0 -0
  181. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain.py +0 -0
  182. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain_lifecycle.py +0 -0
  183. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_crosschain_recovery.py +0 -0
  184. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_euler_v2.py +0 -0
  185. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_foundry.py +0 -0
  186. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_guide.py +0 -0
  187. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_hypercore_model.py +0 -0
  188. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_hypercore_shadow.py +0 -0
  189. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_simulate.py +0 -0
  190. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/_validation.py +0 -0
  191. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/addresses.py +0 -0
  192. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/conftest.py +0 -0
  193. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/constants.py +0 -0
  194. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  195. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/hypercore_market55.json +0 -0
  196. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/clean.json +0 -0
  197. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  198. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/rich.json +0 -0
  199. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  200. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_about.py +0 -0
  201. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_access_manager.py +0 -0
  202. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cache.py +0 -0
  203. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_chains.py +0 -0
  204. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_changelog.py +0 -0
  205. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_commands.py +0 -0
  206. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_config_store.py +0 -0
  207. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_hypercore.py +0 -0
  208. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_lending_health.py +0 -0
  209. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_oracle_mapping.py +0 -0
  210. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_pending_requests_scan.py +0 -0
  211. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_role_accounts.py +0 -0
  212. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_info_golden.py +0 -0
  213. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_predates_getters.py +0 -0
  214. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_cli_vault_validate.py +0 -0
  215. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_contract_encoder.py +0 -0
  216. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_ccip_events.py +0 -0
  217. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_contracts.py +0 -0
  218. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_encoding.py +0 -0
  219. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_errors.py +0 -0
  220. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_layering.py +0 -0
  221. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_readiness.py +0 -0
  222. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_crosschain_readiness_report.py +0 -0
  223. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_erc20.py +0 -0
  224. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_composed_hypercore.py +0 -0
  225. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_crosschain.py +0 -0
  226. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  227. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_examples_vaults.py +0 -0
  228. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_explorer.py +0 -0
  229. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_external_state_events.py +0 -0
  230. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fee_manager.py +0 -0
  231. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fuse_encoding.py +0 -0
  232. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_fusion_instance.py +0 -0
  233. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_guide.py +0 -0
  234. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_deployment.py +0 -0
  235. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_events.py +0 -0
  236. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_flow.py +0 -0
  237. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_live.py +0 -0
  238. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_hypercore_simulation.py +0 -0
  239. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_logs.py +0 -0
  240. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_hypercore.py +0 -0
  241. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_models.py +0 -0
  242. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_mcp_vault_validate.py +0 -0
  243. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_multicall.py +0 -0
  244. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_oracle.py +0 -0
  245. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_oracle_mapping.py +0 -0
  246. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_plasma_vault.py +0 -0
  247. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_reader_encoding.py +0 -0
  248. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_readiness_selection.py +0 -0
  249. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_revert_decoding.py +0 -0
  250. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_rewards_manager.py +0 -0
  251. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_roles.py +0 -0
  252. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_sdk_layering.py +0 -0
  253. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  254. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  255. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  256. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_canary.py +0 -0
  257. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_hypercore.py +0 -0
  258. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  259. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  260. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  261. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  262. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  263. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_arbitrum.py +0 -0
  266. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_examples_crosschain.py +0 -0
  268. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_external_state_executor_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  270. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_morpho_blue.py +0 -0
  273. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v3.py +0 -0
  274. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  275. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  276. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  277. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  278. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_readers_integration.py +0 -0
  279. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_release_funds.py +0 -0
  280. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_swap_on_base.py +0 -0
  281. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  282. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_solidity_compiler.py +0 -0
  283. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_solidity_mirrors.py +0 -0
  284. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_substrates.py +0 -0
  285. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_types_and_events.py +0 -0
  286. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_unpriceable_tokens.py +0 -0
  287. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_validation.py +0 -0
  288. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_dep_graph.py +0 -0
  289. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_info_onchain_base.py +0 -0
  290. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  291. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_web3_context.py +0 -0
  292. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/test_withdraw_manager.py +0 -0
  293. {ipor_fusion-4.0.0.dev78 → ipor_fusion-4.0.0.dev86}/tests/typed_client/consumer.py +0 -0
  294. {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. A key the parser requires can instead raise `KeyError`, e.g. a `loanAsset`
224
- without `address`. When matching a market's own allocation and flow cap, an id that
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`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev78
3
+ Version: 4.0.0.dev86
4
4
  Summary: The IPOR Fusion SDK for Python
5
5
  Project-URL: homepage, https://ipor.io
6
6
  Project-URL: repository, https://github.com/IPOR-Labs/ipor-fusion.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev78"
3
+ version = "4.0.0.dev86"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -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 ContractNotFoundError, NotPlasmaVaultError
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() — either an empty eth_call return
193
- (InsufficientDataBytes on decode) or a revert (ContractLogicError).
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 RuntimeError(
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 RuntimeError("Call.decode() on a write-only Call.")
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:
@@ -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 ValueError(
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 ValueError(f"storage word of {len(data)} bytes, more than a 32-byte slot")
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 ``ValueError`` rather than passing as
234
- a plausible value; a zero executor word is ``None`` (no executor deployed),
235
- which is a state, while an RPC or decoding failure raises.
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 ValueError(
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 ValueError(
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 `ValueError` when no executor is deployed yet (the slot reads
313
- zero) rather than returning a wrapper around the zero address, and lets
314
- `decode` reject a slot whose high 12 bytes are non-zero (a wrong or
315
- corrupt slot, not an address-in-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
- # Left-pad so an RPC that strips leading zero bytes still decodes; the
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
- ).rjust(32, b"\x00")
324
- executor = Web3.to_checksum_address(decode(["address"], raw)[0])
325
- if executor == ZERO_ADDRESS:
326
- raise ValueError(
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, executor)
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, was not emitted by this executor, carries a hash that
638
- is not the hash of its own fields, or whose data is malformed -- and for
639
- a call with no chain id available at all; `TypeError` for `data` that is
640
- not hex at all, which is the one field whose shape it cannot rule out
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 ValueError("BalanceProposed log carries no data")
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 ValueError(f"malformed BalanceProposed data: {exc}") from exc
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
- expected_emitter = self._require_address()
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
- return found
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.find_balance_proposed(
844
- propose_receipt["logs"], balance_account, chain_id=confirmer_ctx.chain_id
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 ValueError(
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 ValueError(
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 SimulationError, decode_custom_error
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 RuntimeError(f"eth_simulateV1 failed: {err}")
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 RuntimeError(
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 ``ValueError`` when no slot up
751
- to ``max_slot`` answers, which is the case for tokens whose balances are not
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(no_slot)
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 ValueError(no_slot)
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(
@@ -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 ``ValueError``
122
- before reading static config when the OnRamp version has an unverified
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 ValueError(
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
  )
@@ -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, or the indexers were unreachable); the message says which."""
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
- results = (response.json() or {}).get("results") or []
81
- if not results:
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 = results[0]["verifierResult"]
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,