ipor-fusion 4.0.0.dev137__tar.gz → 4.0.0.dev139__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 (299) hide show
  1. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/MIGRATION_4.md +546 -434
  2. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/PKG-INFO +24 -9
  3. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/README.md +23 -8
  4. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/pyproject.toml +1 -1
  5. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/skills/ipor-deploy-vault/SKILL.md +17 -12
  6. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/guide/architecture.md +6 -3
  7. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/guide/glossary.md +3 -3
  8. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/guide/invariants.md +1 -1
  9. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/guide/quickstart.md +16 -11
  10. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_guide.py +3 -1
  11. ipor_fusion-4.0.0.dev139/tests/test_docs_typing.py +72 -0
  12. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.claude-plugin/marketplace.json +0 -0
  13. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.claude-plugin/plugin.json +0 -0
  14. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.env.example +0 -0
  15. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.gemini/settings.json +0 -0
  16. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.git-blame-ignore-revs +0 -0
  17. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/cd.yml +0 -0
  18. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/ci.yml +0 -0
  19. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/commit-lint.yml +0 -0
  20. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/notify-slack.yml +0 -0
  21. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/python-build.yml +0 -0
  22. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/python-publish.yml +0 -0
  23. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/release.yml +0 -0
  24. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.github/workflows/report-slack.yml +0 -0
  25. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.gitignore +0 -0
  26. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.mcp.json +0 -0
  27. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.pre-commit-config.yaml +0 -0
  28. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/.python-version +0 -0
  29. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/AGENTS.md +0 -0
  30. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/CHANGELOG.md +0 -0
  31. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/CLAUDE.md +0 -0
  32. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/CODEOWNERS +0 -0
  33. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/LICENSE +0 -0
  34. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/build.sh +0 -0
  35. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/CONTRIBUTING.md +0 -0
  36. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/README.md +0 -0
  37. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  38. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  39. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  40. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  41. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/external_state_margin_leg_base.py +0 -0
  42. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/examples/simple_aave_v3_supply_base.py +0 -0
  43. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/pytest.ini +0 -0
  44. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/scripts/commit-check.sh +0 -0
  45. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/scripts/crosschain_readiness.py +0 -0
  46. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/__init__.py +0 -0
  47. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/about.py +0 -0
  48. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cache.py +0 -0
  49. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/chains.py +0 -0
  50. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/__init__.py +0 -0
  51. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  52. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/config_cmd.py +0 -0
  53. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/config_store.py +0 -0
  54. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/main.py +0 -0
  55. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/market_cmd.py +0 -0
  56. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/options.py +0 -0
  57. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  58. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  59. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/vault_health.py +0 -0
  60. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  61. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  62. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  63. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/config/__init__.py +0 -0
  64. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/config/roles.py +0 -0
  65. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/__init__.py +0 -0
  66. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/access.py +0 -0
  67. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/address.py +0 -0
  68. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/context.py +0 -0
  69. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/contract.py +0 -0
  70. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/erc20.py +0 -0
  71. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/external_state_executor.py +0 -0
  72. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/fee_manager.py +0 -0
  73. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/fuse.py +0 -0
  74. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/fuse_action.py +0 -0
  75. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/fusion_factory.py +0 -0
  76. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/logs.py +0 -0
  77. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/multicall.py +0 -0
  78. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/oracle.py +0 -0
  79. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/plasma_vault.py +0 -0
  80. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/rewards_manager.py +0 -0
  81. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/simulation.py +0 -0
  82. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  83. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/__init__.py +0 -0
  84. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  85. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  86. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  87. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  88. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  89. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  90. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  91. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  92. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  93. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/contracts.py +0 -0
  94. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/discovery.py +0 -0
  95. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/errors.py +0 -0
  96. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  97. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  98. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  99. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  100. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/lane.py +0 -0
  101. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/logs.py +0 -0
  102. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/messages.py +0 -0
  103. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/simulation.py +0 -0
  104. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  105. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  106. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  107. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  108. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  109. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/crosschain/transport.py +0 -0
  110. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/dependency_graph.py +0 -0
  111. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/entry_cli.py +0 -0
  112. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/entry_mcp.py +0 -0
  113. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/errors.py +0 -0
  114. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/explorer.py +0 -0
  115. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/field_docs.py +0 -0
  116. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/__init__.py +0 -0
  117. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  118. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/async_action.py +0 -0
  119. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/base.py +0 -0
  120. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  121. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/erc4626.py +0 -0
  122. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  123. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/events.py +0 -0
  124. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/external_state.py +0 -0
  125. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  126. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  127. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  128. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/hypercore.py +0 -0
  129. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  130. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/merkl.py +0 -0
  131. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/morpho.py +0 -0
  132. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  133. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  134. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/fuses/universal.py +0 -0
  135. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/guide/__init__.py +0 -0
  136. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/market_ids.py +0 -0
  137. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/market_info.py +0 -0
  138. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/mcp/__init__.py +0 -0
  139. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/mcp/guide.py +0 -0
  140. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/mcp/models.py +0 -0
  141. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/mcp/server.py +0 -0
  142. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/morpho_api.py +0 -0
  143. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/py.typed +0 -0
  144. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/__init__.py +0 -0
  145. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/aave_v3.py +0 -0
  146. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/aave_v4.py +0 -0
  147. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/compound_v3.py +0 -0
  148. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/hypercore.py +0 -0
  149. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  150. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/lending_health.py +0 -0
  151. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/morpho.py +0 -0
  152. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  153. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  154. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  155. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/position_manager.py +0 -0
  156. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  157. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  158. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/readers/unpriceable.py +0 -0
  159. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/substrates.py +0 -0
  160. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/types.py +0 -0
  161. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/validation.py +0 -0
  162. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_data.py +0 -0
  163. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_erc20.py +0 -0
  164. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_info.py +0 -0
  165. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_managers.py +0 -0
  166. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_positions.py +0 -0
  167. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/src/ipor_fusion/vault_reads.py +0 -0
  168. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/UniswapV3UniversalRouter.py +0 -0
  169. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_crosschain.py +0 -0
  170. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_crosschain_lifecycle.py +0 -0
  171. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_crosschain_recovery.py +0 -0
  172. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_euler_v2.py +0 -0
  173. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_foundry.py +0 -0
  174. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_hypercore_model.py +0 -0
  175. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_hypercore_shadow.py +0 -0
  176. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_multicall.py +0 -0
  177. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_simulate.py +0 -0
  178. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/_validation.py +0 -0
  179. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/addresses.py +0 -0
  180. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/conftest.py +0 -0
  181. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/constants.py +0 -0
  182. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  183. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/hypercore_market55.json +0 -0
  184. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/vault_info_golden/clean.json +0 -0
  185. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  186. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/vault_info_golden/rich.json +0 -0
  187. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  188. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_about.py +0 -0
  189. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_access_manager.py +0 -0
  190. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cache.py +0 -0
  191. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_chains.py +0 -0
  192. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_changelog.py +0 -0
  193. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_commands.py +0 -0
  194. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_config_store.py +0 -0
  195. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_helpers.py +0 -0
  196. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_hypercore.py +0 -0
  197. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_lending_health.py +0 -0
  198. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_market_morpho_blue.py +0 -0
  199. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_oracle_mapping.py +0 -0
  200. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_pending_requests_scan.py +0 -0
  201. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_role_accounts.py +0 -0
  202. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_vault_info_golden.py +0 -0
  203. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_vault_predates_getters.py +0 -0
  204. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_cli_vault_validate.py +0 -0
  205. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_contract_call.py +0 -0
  206. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_contract_encoder.py +0 -0
  207. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_ccip_events.py +0 -0
  208. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_ccip_indexer.py +0 -0
  209. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_ccip_quote.py +0 -0
  210. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_chainlink.py +0 -0
  211. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_contracts.py +0 -0
  212. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_encoding.py +0 -0
  213. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_errors.py +0 -0
  214. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_lane.py +0 -0
  215. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_layering.py +0 -0
  216. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_readiness.py +0 -0
  217. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_readiness_report.py +0 -0
  218. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_crosschain_transport.py +0 -0
  219. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_erc20.py +0 -0
  220. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_error_hierarchy.py +0 -0
  221. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_examples_composed_hypercore.py +0 -0
  222. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_examples_crosschain.py +0 -0
  223. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  224. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_examples_vaults.py +0 -0
  225. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_explorer.py +0 -0
  226. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_external_state_events.py +0 -0
  227. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_external_state_executor.py +0 -0
  228. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_fee_manager.py +0 -0
  229. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_fuse_encoding.py +0 -0
  230. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_fusion_instance.py +0 -0
  231. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_guide.py +0 -0
  232. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_deployment.py +0 -0
  233. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_events.py +0 -0
  234. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_flow.py +0 -0
  235. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_live.py +0 -0
  236. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_outcome.py +0 -0
  237. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_readers.py +0 -0
  238. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_hypercore_simulation.py +0 -0
  239. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_logs.py +0 -0
  240. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_mcp_hypercore.py +0 -0
  241. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_mcp_models.py +0 -0
  242. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_mcp_server.py +0 -0
  243. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_mcp_vault_validate.py +0 -0
  244. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_multicall.py +0 -0
  245. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_oracle.py +0 -0
  246. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_oracle_mapping.py +0 -0
  247. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_plasma_vault.py +0 -0
  248. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_public_api.py +0 -0
  249. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_public_type_reachability.py +0 -0
  250. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_reader_encoding.py +0 -0
  251. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_readiness_selection.py +0 -0
  252. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_revert_decoding.py +0 -0
  253. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_rewards_manager.py +0 -0
  254. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_roles.py +0 -0
  255. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_sdk_layering.py +0 -0
  256. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  257. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  258. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  259. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_crosschain_canary.py +0 -0
  260. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_crosschain_hypercore.py +0 -0
  261. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  262. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  263. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  268. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_examples_arbitrum.py +0 -0
  269. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_examples_base.py +0 -0
  270. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_examples_crosschain.py +0 -0
  271. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_external_state_executor_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  275. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_morpho_blue.py +0 -0
  276. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_pilot_v3.py +0 -0
  277. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  278. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  279. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  280. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  281. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_readers_integration.py +0 -0
  282. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_release_funds.py +0 -0
  283. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_swap_on_base.py +0 -0
  284. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  285. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_solidity_compiler.py +0 -0
  286. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_solidity_mirrors.py +0 -0
  287. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_substrates.py +0 -0
  288. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_types_and_events.py +0 -0
  289. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_unpriceable_tokens.py +0 -0
  290. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_validation.py +0 -0
  291. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_vault_dep_graph.py +0 -0
  292. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_vault_info_onchain_base.py +0 -0
  293. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  294. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_vault_simulator_blocks.py +0 -0
  295. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_web3_context.py +0 -0
  296. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/test_withdraw_manager.py +0 -0
  297. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/typed_client/consumer.py +0 -0
  298. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/tests/typed_client/rejected.py +0 -0
  299. {ipor_fusion-4.0.0.dev137 → ipor_fusion-4.0.0.dev139}/uv.lock +0 -0
@@ -1,336 +1,66 @@
1
1
  # Migrating to ipor-fusion 4.0
2
2
 
3
- 4.0 gives chain, market and role ids, the crosschain, HyperCore and Morpho ids,
4
- vault/oracle/token addresses and transaction hashes a nominal type
5
- (`typing.NewType`) across the public API, and checks the SDK, the examples and a
6
- typed client with pyright's `reportArgumentType`. Other fields keep their types
7
- (e.g. symbols, names and URLs stay `str`).
8
-
9
- The wrappers do not change values: `ChainId(1)` is `1` at runtime. The
10
- [behavior changes](#behavior-changes) below, however, apply to **every** caller,
11
- type-checked or not. In particular, `fetch_market` and
12
- `build_morpho_blue_market_json` no longer add a `0x` prefix themselves: pass
13
- `parse_morpho_market_id(raw_id)`, never a bare 64-hex string. Type-checked code
14
- (pyright, mypy, Pylance) also wraps literals and raw values as below. Values the SDK
15
- returns already carry their type and go back in unwrapped.
16
-
17
- The SDK's own types are importable from `ipor_fusion` (and `ipor_fusion.types`),
18
- as are `USDC_TOKEN_INDEX` and `hip3_action_asset`. Third-party types are not
19
- re-exported: `ChecksumAddress` and `HexAddress` come from `eth_typing`, and
20
- `BlockIdentifier`, `Timestamp` and `TxParams` from `web3.types`. The wrappers label
21
- values; they do not validate byte widths or address spelling.
22
-
23
- ## Building the types
24
-
25
- | Type | From a literal or raw value |
26
- |---|---|
27
- | `ChainId` | `ChainId(1)` |
28
- | `MarketId` | an `IporFusionMarkets` member, or `MarketId(n)` |
29
- | `RoleId` | a `Roles` member (accepted as is), or `RoleId(n)`; `Roles.resolve(name)` returns one |
30
- | `Decimals`, `Period`, `Amount`, ... | `Decimals(8)`, `Period(0)`, `Amount(n)` (unchanged) |
31
- | `ChecksumAddress` | `Web3.to_checksum_address(value)` |
32
- | `HexAddress` | any `ChecksumAddress`, or `parse_hex_address(value)` (20 bytes or `0x`/`0X`-prefixed hex in any case → lowercase `0x` form) |
33
- | `MorphoBlueMarketId` | `parse_morpho_market_id(value)`: 32 bytes or hex in any spelling, always `0x` + 64 lowercase digits |
34
- | `TxHash` | `parse_tx_hash(value)` (32 bytes or `0x`/`0X`-prefixed hex in any case) |
35
- | `PlasmaVaultAddress`, `FuseAddress`, `AccessManagerAddress` | from the SDK (`PlasmaVault.address`, `get_fuses()`, `get_access_manager_address()`, `FusionInstance`, discovery), or `parse_plasma_vault_address` / `parse_fuse_address` / `parse_access_manager_address(value)` for a raw address; they label the role and check nothing on chain |
36
- | crosschain ids | `CommandId(b)`, `MessageId(b)`, `AssetId(b)`, `OperationId(b)`, `TransferId(b)`, `ReturnRequestId(b)`, `PositionSetHash(b)` (bytes32); `ProposalId(n)`, `DeploymentRequestId(n)`, `StargateAssetId(n)`, `LayerZeroEid(n)`, `CcipChainSelector(n)` (int) |
37
- | HyperCore ids | `HyperCoreTokenIndex(n)` (or `USDC_TOKEN_INDEX`), `HyperCoreAsset(n)` (or `hip3_action_asset(dex, index)`), `HyperCoreOrderId(n)`, `HyperCoreClientOrderId(n)`, `MarginTableId(n)`, `HyperCoreActionId(n)`, `PayloadHash(b)` |
38
- | external state | `ProposalHash(b)` |
39
- | `BlockIdentifier` | a block number, `"latest"` / `"pending"` / ..., or a block hash |
40
-
41
- The address and hash parsers (`parse_hex_address`, `parse_tx_hash`,
42
- `parse_plasma_vault_address`, `parse_fuse_address`, `parse_access_manager_address`)
43
- validate strictly: a string needs exactly one `0x`/`0X` prefix and the exact number of
44
- hex digits, with no surrounding whitespace; anything else raises `ValueError`. Strip
45
- user input yourself before parsing it. `parse_morpho_market_id` still accepts hex
46
- without `0x`.
47
-
48
- `MessageId` covers both a CCIP `messageId` and a LayerZero `guid`. `AssetId` is the
49
- crosschain executor's `bytes32` asset id; a Stargate pool's `uint16` asset id is
50
- `StargateAssetId`. `DeploymentRequestId` is a factory's `uint256` dispatcher request;
51
- a `bytes32` return (recall) request is `ReturnRequestId`.
52
-
53
- ## What changed, by area
54
-
55
- **Public API.** 4.0 declares it: `ipor_fusion.__all__`, which now also exports the
56
- names earlier reachable only by module path (`FusionFactory`, `FusionInstance`,
57
- `fetch_market`, `resolve_token_symbol`, the crosschain codecs, ...). Import from
58
- `ipor_fusion`: a module path outside the README's list (*Public API and stability*)
59
- still works but is internal. Every project type a public signature or field uses is
60
- exported too, among them `AaveV3PositionBreakdown`, `AaveV3ReserveTokens`,
61
- `AccountRequest`, `StateOverrideProvider`, `TransferDirection` and the validation
62
- vocabulary `Severity`, `Verdict` and `Check`. Also new: `InFlightLimit` (below) and
63
- `log_bytes`, the hex-or-bytes log field helper the crosschain decoders use.
64
-
65
- **Vault reads and validation.** The vault read is one pinned snapshot, and the
66
- chain is always the context's:
67
-
68
- - `fetch_vault_data(ctx, vault, *, block=None, lending=True, metadata=True)` takes
69
- the vault address (not a `PlasmaVault`) and no `chain_id`. It resolves one block
70
- (``block``, else a numeric `ctx.default_block`, else the latest) and runs every
71
- read, log scans included, on `ctx.at_block(block)`; `ctx` is not modified. The old
72
- ``chain_id=0`` switch is ``lending=False``; ``metadata=False`` skips the
73
- display metadata: `token_symbols` and `token_decimals` stay empty and the ERC20
74
- holdings' symbols are `"?"`. The underlying asset's symbol and decimals and the
75
- decimals the valuation needs are still read. With ``metadata=True`` the holdings
76
- are named by the same cached, batched read as `token_symbols`. `VaultData` gains `vault`, `chain_id`, `market_balances`, `erc20`,
77
- `token_symbols` and `token_decimals`; `WithdrawManagerData` gains
78
- `shares_to_release_assets` and `pending_request_assets`.
79
- - `validate_vault(ctx, vault, *, block=None, metadata=False)` fetches (by default
80
- without display metadata) and validates; it no longer takes ``resolve_symbol``.
81
- ``metadata=True`` names the ERC20 holdings in the result and its findings. `validate_vault_state(state)` is
82
- pure: a custom `VaultState` also provides `vault`, `chain_id`, `market_balances`,
83
- `erc20` and the queue's `shares_to_release_assets`. `read_erc20_holdings` is gone:
84
- the holdings are `VaultData.erc20`.
85
- - `build_vault_info_json(data, *, resolve_name=None)` renders from the snapshot and
86
- reads nothing; the chain and its label come from `data.chain_id`.
87
- - `fetch_deployment_info(ctx, vault, *, api_key=None)` and
88
- `fetch_vault_lending_health(ctx, vault, *, balance_fuse_market_ids,
89
- market_substrates, aave_pools=None)` take the chain from `ctx`.
90
- - `Web3Context(web3, chain_id, *, ...)` and `Web3Context.from_url(url, *, ...)` take
91
- their options by keyword. New: `Web3Context.at_block(block)` (a pinned copy) and
92
- `Call.call(ctx, *, block=None)` (see [Context-free wrappers](#context-free-wrappers)). `Web3Context.get_logs(to_block=None)` ends at
93
- the context's default block and `Web3Context.get_block(block=None)` returns it,
94
- both instead of ``"latest"``.
95
- - `Web3Context.default_block` is read-only: `ctx.default_block = n` raises
96
- `AttributeError`. Pin a copy instead, `ctx = ctx.at_block(n)`; the old setter
97
- changed the block under every thread sharing the context.
98
-
99
- Snapshot fields carry their units: `VaultData.total_assets`,
100
- `underlying_balance_on_vault` and `market_balances` values are `Amount`;
101
- `total_supply` and `supply_cap` are `Shares`; `share_decimals` / `asset_decimals` are `Decimals`;
102
- `block_timestamp` and `deployment_timestamp` are `Timestamp`. `FeeData` fee fields
103
- are `Fee`, `management_fee_last_update` a `Timestamp`, `unrealized_management_fee` an
104
- `Amount`; `WithdrawManagerData.withdraw_window` is a `Period`, its fees `Fee`,
105
- `shares_to_release` `Shares`, `last_release_funds_timestamp` a `Timestamp` and the
106
- converted amounts `Amount`. A custom `VaultState` annotates the same.
107
- `VaultValidation.block_timestamp` is a `Timestamp` and `asset_decimals` `Decimals`;
108
- `Erc20Holding.decimals` / `balance` are `Decimals | None` / `Amount | None` and
109
- `Erc20Holdings.value_in_asset` / `market_cached_value` `Amount`;
110
- `VaultData.token_decimals` values and `resolve_token_decimals` are `Decimals | None`.
111
- `PlasmaVault.total_supply()`, `balance_of()` and `get_total_supply_cap()` return
112
- `Shares` (vault shares), and `transfer` / `approve` / `transfer_from` /
113
- `set_total_supply_cap` take `Shares`; `ERC20` keeps `Amount`. The supply cap is
114
- now shown in share decimals labelled `shares` (``fusion vault info``) and its JSON
115
- ``formatted`` uses `share_decimals`; it was formatted with the asset's decimals.
116
-
117
- `Web3Context.from_url(..., max_concurrent_requests=8)` bounds the RPC requests in
118
- flight across every thread using the context (``None`` lifts the bound); the vault
119
- reads fan out on nested thread pools, and the bound is what a rate-limited provider
120
- sees. An `int` bounds one context. To bound several contexts on one provider (a
121
- service that builds a context per request or per scan), create one
122
- `InFlightLimit(n)` per provider and pass it as `max_concurrent_requests=`: every
123
- context built with it shares the same `n` slots. A context built from your own
124
- `Web3` object is not bounded unless you add the limit to it:
125
- `limit.install(web3)` (innermost, as `from_url` does) or
126
- `web3.middleware_onion.inject(limit.middleware, layer=0)`.
127
-
128
- `ensure_supported_chain` takes `ChainId`. `fetch_unpriceable_priced_tokens(ctx, *,
129
- price_oracle, underlying, balance_fuses, market_substrates, morpho_positions)` takes
130
- everything after `ctx` by keyword, the oracle and underlying as `ChecksumAddress`.
131
- `resolve_token_symbol`, `resolve_token_decimals`,
132
- `get_contract_name` and `get_deployment_tx` take `HexAddress` (a checksum address
133
- fits) and `api_key` by keyword only (`get_contract_name(chain_id, address, *,
134
- api_key=None, cache=None)`, `get_deployment_tx(chain_id, address, *, api_key=None)`),
135
- and the `resolve_name` / `resolve_symbol` callbacks receive one (a callback
136
- typed for `str` still fits). Market-keyed data is keyed by `MarketId`:
137
- `VaultData.market_substrates`, `dependency_graph`, `morpho_positions`,
138
- `aave_positions`, `market_balances`; the `VaultState` protocol, `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens`
139
- and the `dependency_graph` helpers (`compute_update_reach`, `compute_update_groups`,
140
- `find_orphan_fuse_markets`, `find_markets_missing_erc20_dependency`).
141
- `find_orphan_fuse_markets` returns `dict[MarketId, list[FuseAddress]]`, so its
142
- fuses go into `PlasmaVault.remove_fuses` as they are.
143
-
144
- `VaultData.price_oracle_addr` is renamed `price_oracle`, like its sibling
145
- address fields (a custom `VaultState` renames it too).
146
- `VaultData.asset`, `price_oracle`, `rewards_manager`, `withdraw_manager`,
147
- `FeeData.fee_manager` / `ipor_dao_fee_recipient` and `VaultValidation.vault` /
148
- `asset` are `ChecksumAddress` (`VaultValidation.vault` a `PlasmaVaultAddress`);
149
- `VaultData.access_manager` is an `AccessManagerAddress`, `fuses` / `instant_fuses`
150
- and the `fuse_markets` keys are `FuseAddress`, `balance_fuses` is
151
- `list[BalanceFuse]`, and `token_prices_usd` is keyed by lowercase `HexAddress`. A
152
- custom `VaultState` implementation annotates `asset` and `price_oracle` as
153
- `ChecksumAddress`, `fuse_markets` keys as `FuseAddress`, and its
154
- `unpriceable_priced_tokens` items' `market_id` as `MarketId` (`UnpriceableToken`).
155
- The Uniswap V3 / Ramses V2 position events' `version`, `sender` and `recipient` are
156
- `ChecksumAddress`. `VaultValidation.chain_id` is `ChainId`;
157
- `LendingMarketHealth.market_id` and `MiddlewarePricedToken.market_id` are
158
- `MarketId`; `Erc20Holding.address` is `HexAddress`, and `SubstrateInfo.address` is
159
- `HexAddress | None` (`None`, no longer `""`, when a substrate carries no address).
160
- `LegacyPriceOracleMiddleware` takes `base_currency_decimals: Decimals`, and
161
- `PriceOracleMiddleware.base_currency_decimals()` returns `Call[Decimals]`.
162
- `get_logs_adaptive(chain_id=)` and `LogSource.get_logs` take `ChainId`;
163
- `quote_ccip_native_fee(block=)` and `OracleMappingReader.asset_source_events` take a
164
- `BlockIdentifier`.
165
-
166
- **Address roles.** `PlasmaVault(address)`,
167
- `validate_vault`, `fetch_deployment_info`, the HyperCore, lending-health and
168
- oracle-mapping readers, `read_external_state`, `VaultSimulator(vault=)`,
169
- `CrosschainSimulator`, crosschain discovery, the hub-manager inputs
170
- (`ExecutorInitParams.manager`, `create_executor(manager=)`, `executors_of(manager=)`),
171
- the async command helpers `Command.request_shares` / `redeem_from_request`,
172
- `pending_request_shares` and `WithdrawManager.update_plasma_vault_address` take a
173
- `PlasmaVaultAddress`; `.address`, `FusionInstance.plasma_vault` and the executor's
174
- `manager()` return one. Crosschain deposit and redeem destinations stay
175
- `ChecksumAddress`: `Command.deposit` / `redeem`, `is_allowed_vault`,
176
- `is_tracked_vault` and the remote-vault substrate accept any allowlisted ERC4626.
177
- Every fuse constructor, `FuseAction.fuse`, `BalanceFuse.fuse`,
178
- `PlasmaVault.add_fuses` / `remove_fuses` / `add_balance_fuse` / `remove_balance_fuse`
179
- / `balance_fuse_value`, `RewardsManager.add_reward_fuses` / `is_reward_fuse_supported`,
180
- `LaneFuses`, the HyperCore balance-fuse argument and the
181
- `PlasmaVault.configure_instant_withdrawal_fuses` configs
182
- (`Sequence[tuple[FuseAddress, Sequence[bytes]]]`) take a `FuseAddress`, and
183
- `get_fuses()`, `get_instant_withdrawal_fuses()`, `get_rewards_fuses()` and
184
- `VaultData.fuses` / `instant_fuses` / `fuse_markets` keys return it.
185
- `AccessManager(address)` takes an `AccessManagerAddress`;
186
- `get_access_manager_address()`, `VaultData.access_manager` and
187
- `FusionInstance.access_manager` return one. `resolve_access_manager` still takes any
188
- `ChecksumAddress`: it is what checks that the address is a vault.
189
-
190
- The vault parameter is named `vault` throughout: `validate_vault`,
191
- `fetch_deployment_info`, `fetch_vault_lending_health`, `build_oracle_mapping`,
192
- `read_external_state`, `ExternalStateExecutor.for_vault`, `read_hypercore_nav`,
193
- `read_hypercore_vault_state`, `read_hypercore_transfer_snapshot`,
194
- `read_hypercore_evm_exit_ceiling` and `resolve_access_manager` renamed
195
- ``vault_address`` to ``vault``; only a call that passes it by keyword changes.
196
-
197
- **Simulation and factory records.** `VaultSimulator(web3, *, vault=None, alpha=None,
198
- block=...)` takes every argument after `web3` by keyword, and `vault` and `alpha` are
199
- optional: a simulator that only observes, deploys or sends `execute_call(call,
200
- from_=...)` needs neither, so drop a zero-address placeholder. `execute()` without
201
- `vault` and `alpha` raises `ValueError`, as does `execute_call` without `from_` or
202
- `alpha`. `FusionInstance.from_dict(data)` rebuilds a stored instance (the
203
- `dataclasses.asdict` form) with every field parsed; use it instead of
204
- `FusionInstance(**data)`, which passes raw strings into the role-typed fields.
205
-
206
- **Transactions, LayerZero and constants.** `Web3Context.build_transaction` and
207
- `Call.build_transaction` return web3's `TxParams` (the same keys; `data` is `0x` hex,
208
- the fee fields are `Wei`). LayerZero endpoint ids are `LayerZeroEid`:
209
- `StargateChain.eid`, `Packet.src_eid` / `dst_eid`, `EnforcedOptionParam.eid`,
210
- `executor_eid()` and `eid_of()` return it and `chain_id_of`, `peers`, `lz_receive_calldata(src_eid=)`
211
- and the Stargate fuse's `eid=` take it (`LayerZeroEid(30101)`). CCIP chain
212
- selectors are `CcipChainSelector`: `CcipChain.chain_selector`,
213
- `CcipRouteConfig.chain_selector`, `CcipTokenLane.chain_selector`, the `MessageV1`,
214
- `CcipMessageSent` and `Any2EVMMessage` selectors and `CcipRouter.get_off_ramps()`
215
- carry it, and `chain_id_of_selector`, `CcipRouter.is_chain_supported` /
216
- `get_on_ramp`, `CcipTokenPool.is_supported_chain` and `ccip_token_lane` take it
217
- (`CcipChainSelector(4949039107694359620)`).
218
- `encode_oft_compose_msg(*, nonce=, src_eid=, amount_ld=, compose_msg=)` takes
219
- keyword arguments only. `ZERO_ADDRESS` is a `ChecksumAddress` (same value).
220
-
221
- **Chain registry and removed aliases.** `CHAIN_NAMES`, `CHAIN_NAME_TO_ID`,
222
- `SUPPORTED_CHAIN_IDS`, `GET_LOGS_RANGE_HINTS`, `BLOCK_EXPLORER_URLS`,
223
- `MORPHO_BLUE_ADDRESSES`, `AAVE_V3_POOL`, `PUBLIC_ALLOCATOR_ADDRESSES` and
224
- `HYPERCORE_PENDING_READERS` are keyed by `ChainId` (`HYPEREVM_CHAIN_ID` is one) and
225
- `explorer.SUPPORTED_CHAINS` is a `frozenset[ChainId]`, so
226
- `ensure_supported_chain(CHAIN_NAME_TO_ID["ethereum"])` type-checks;
227
- `PUBLIC_ALLOCATOR_ADDRESSES` values are `ChecksumAddress`. Import `CHAIN_NAMES`,
228
- `CHAIN_NAME_TO_ID`, `SUPPORTED_CHAIN_IDS`, `GET_LOGS_RANGE_HINTS`,
229
- `PUBLIC_ALLOCATOR_ADDRESSES`, `HYPERCORE_PENDING_READERS`, `HYPEREVM_CHAIN_ID` and
230
- `HYPERCORE_MARKET` from `ipor_fusion` (the HyperCore ones also from
231
- `ipor_fusion.readers`). `BLOCK_EXPLORER_URLS`, `MORPHO_BLUE_ADDRESSES`,
232
- `AAVE_V3_POOL` and `explorer.SUPPORTED_CHAINS` are internal: use
233
- `morpho_blue_address(chain_id)` for Morpho Blue. `IporFusionMarkets.RWA`
234
- is gone: market 50 is `IporFusionMarkets.EXTERNAL_STATE`. `MORPHO_BLUE_ADDRESS` is
235
- gone: use `morpho_blue_address(chain_id)`.
236
-
237
- **Markets and substrates.** Every `IporFusionMarkets` member is a `MarketId`.
238
- `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
239
-
240
- **Access.** `AccessManager.has_role`, `grant_role`, `revoke_role`,
241
- `read_accounts_with_role` take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
242
-
243
- **Morpho.** `MorphoBlueMarketId` now always carries `0x`; build it with
244
- `parse_morpho_market_id` instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market`,
245
- `build_morpho_blue_market_json` take it; `MorphoApiMarket.market_id`,
246
- `VaultV1MarketAllocation.market_id`, `VaultV2Cap.market_id`,
247
- `MorphoPositionBreakdown.market_id` (as read from substrates) and the keys of
248
- `VaultV1Info.public_allocator_flow_caps` are canonical. `fetch_market`, `fetch_vault`,
249
- `morpho_blue_address`, `build_*_json` take `ChainId`; `fetch_vault` takes a
250
- `ChecksumAddress`. `build_morpho_blue_market_json(*, market_id, chain_id, params,
251
- state, rates, api_market=None, api_error=None, public_allocator=None)` takes every
252
- argument by keyword, `public_allocator` as `ChecksumAddress | None`.
253
- Every address on the `morpho_api` records is `ChecksumAddress`;
254
- optional ones (`curator`, `guardian`, `fee_recipient`, `performance_fee_recipient`,
255
- `management_fee_recipient`, `inner_vault`, `VaultFlowCap.admin`, the `VaultV2Cap`
256
- market params) are `ChecksumAddress | None`.
257
-
258
- **Crosschain.** The executor, dispatcher and factory wrappers, the lanes and
259
- `StargateCrosschainCommandFuse.enter(request_id=)` take the crosschain ids above, and
260
- their getters return them (`active_command`, `active_proposal_id`, `propose_balance`
261
- on CCIP, `active_return` → `OperationId`, `asset_id`, `tracked_position_set_hash`,
262
- `pending_request_by_chain`, `CrosschainLane.proposal_id_from_logs` /
263
- `proposal_id_from_receipt`). Records carry them too: `Command`, `ActiveCommand`,
264
- `ExecutorInitParams`, `BalanceObservation`, `LaneObservation`, `Observation`,
265
- `CcipObservation`, `OutboundMessage`, `CcipMessageSent`, `Any2EVMMessage`,
266
- `VerifierResult`, `Packet.guid`, `TaxiMessage`, `StargateChain`; and the helpers
267
- `fetch_verifier_result(message_id=)`, `lz_receive_calldata(guid=)` and
268
- `lz_compose_calldata(guid=)` take a `MessageId`. Decoded event values
269
- (`ccip_events` → `CcipEvent.values`) stay a generic mapping: convert a value
270
- before passing it on.
271
-
272
- **HyperCore (preview).** The fuses, `HyperCoreSubstrates`, `HyperCoreReader`,
273
- `read_hypercore_transfer_snapshot` and the helpers `system_address`, `precompile_index_of`
274
- (3.x `read_index_of`, renamed: it reads nothing),
275
- `perp_dex_of`, `is_native_perp_asset`, `is_hip3_asset` take the HyperCore ids;
276
- `HyperCorePerpAssetInfo`, `HyperCorePerpMarket`, `HyperCoreSpotLeg`,
277
- `HyperCorePendingState` and `HyperCoreTransferSnapshot` carry them.
278
- `HyperCorePendingReader(vault, reader)` takes the vault's `PlasmaVaultAddress`, not a
279
- `PlasmaVault` wrapper.
280
-
281
- **External state.** `ExternalStateExecutor.proposal_hash` returns and `confirm_balance`
282
- takes a `ProposalHash`; `BalanceProposal.proposal_hash` is a `ProposalHash`;
283
- `ExternalStateEvent.transaction_hash` is a `TxHash`.
284
- `ExternalStateVaultState.last_checked_custodian_timestamp`,
285
- `BalanceFuseSnapshot.last_custodian_update_timestamp`, `BalanceProposal.proposed_at`,
286
- `last_updated()` and `get_oldest_update_timestamp()` are web3's `Timestamp`, and
287
- `ExternalStateExecutor.proposal_hash(proposed_at=)` takes one.
288
- `ExternalStateExecutor.mark_nav` and `NavMark` are gone: send `propose_balance` and
289
- `confirm_balance` from separate custodians, then
290
- `PlasmaVault.update_markets_balances([IporFusionMarkets.EXTERNAL_STATE])`.
291
-
292
- **Errors and explorer.** `TransactionError(tx_hash=)` takes a `TxHash`,
293
- `LogScanError(address=)` a `ChecksumAddress`; `get_revert_reason` takes a `TxHash`;
294
- `get_deployment_tx` returns a `TxHash`
295
- (a malformed hash from Etherscan is reported as the error `malformed-tx-hash`).
296
-
297
- **MCP response models** (Python constructors only; the JSON schemas are unchanged
298
- except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
299
- `MarketId`. Calling an MCP tool function directly from Python takes `ChainId`.
300
-
301
- **Fuse base.** `Fuse`, `StakeFuse` and the shared validators and substrate packers
302
- moved to `ipor_fusion.core.fuse`. Import `Fuse` and `StakeFuse` from `ipor_fusion`
303
- or `ipor_fusion.fuses`. `ipor_fusion.fuses.base` still re-exports them in 4.0, but
304
- it is a submodule of a public subpackage and therefore internal (see the README,
305
- *Public API and stability*); so are the underscore validators and packers. Fuse equality and
306
- hash now cover every attribute a fuse stores, not only its class and address (see
307
- [Behavior changes](#behavior-changes)).
308
-
309
- **Crosschain fuse encoders moved.** The crosschain market encoders moved from
310
- `ipor_fusion.fuses.crosschain` into the crosschain package, and
311
- `ipor_fusion.fuses` no longer re-exports them. There is no shim: the old paths
312
- raise `ImportError`. The top-level names (`from ipor_fusion import ...`) are
313
- unchanged.
314
-
315
- | 3.x import | 4.0 import |
316
- |---|---|
317
- | `ipor_fusion.fuses.crosschain` | `ipor_fusion.crosschain.fuses` (or `ipor_fusion.crosschain`) |
318
- | `ipor_fusion.fuses.crosschain.base`, `.ccip`, `.stargate` | `ipor_fusion.crosschain.fuses` (its submodules `base`, `ccip` and `stargate` exist but are internal) |
319
- | `from ipor_fusion.fuses import CcipCrosschainSupplyFuse` (and the other crosschain encoders, `SendParams`, `CrosschainSubstrate*`, `crosschain_market_id`) | `from ipor_fusion.crosschain import ...` or `from ipor_fusion import ...` |
320
-
321
- `ipor_fusion.crosschain` now exports the whole crosschain surface: besides the
322
- codecs and wrappers, the encoders, `CrosschainLane`, `CcipLane`, `StargateLane`,
323
- the transports and chains, `discover_deployment`, `open_lane(s)`, `LANES` and
324
- `CrosschainSimulator`, which 3.x exported only from `ipor_fusion`.
325
- `ipor_fusion.crosschain.ccip` and `.stargate` keep exporting only their codecs
326
- and wrappers.
327
-
328
- **Moved, still importable.** `FuseAction` lives in `ipor_fusion.core.fuse_action` and
329
- `ZERO_ADDRESS` in `ipor_fusion.types`; import both from `ipor_fusion` (`FuseAction`
330
- also from `ipor_fusion.fuses`, `ZERO_ADDRESS` from the public module
331
- `ipor_fusion.types`). The internal `ipor_fusion.fuses.base` still re-exports them. The CLI's internal
332
- `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
333
- `vault_markets_view` and `oracle_mapping_view` (private names, not API).
3
+ 4.0 changes how every program calls the SDK, not only how type-checked code
4
+ spells its arguments. Start with the checklist below; the sections after it hold
5
+ the details, and each change is described once, in its section.
6
+
7
+ ## Start here
8
+
9
+ > **A 3.x program fails when a line runs, not when it is imported.**
10
+ > `PlasmaVault(ctx, address)`, `ERC20(ctx, token)` or any other `Wrapper(ctx, ...)`
11
+ > raises `TypeError` only when that line executes, and so does `call.call()` without
12
+ > a context. A service that wraps its reads in a broad `except Exception` degrades
13
+ > silently (empty values, skipped vaults) instead of failing. Before you deploy:
14
+ >
15
+ > - search for wrappers built with a context (`(ctx,`, `(self.ctx,`, `(self._ctx,`)
16
+ > and for `.call()`, `.send()`, `.build_transaction()` and `.estimate_gas()` with no
17
+ > argument;
18
+ > - look for caches or containers of wrappers typed `Any` (or untyped): a type
19
+ > checker cannot see the calls made through them;
20
+ > - run the reads against a provider, not only the test suite.
21
+
22
+ Work through these in order; each is what breaks first at run time after the one
23
+ before it is fixed.
24
+
25
+ 1. **Wrappers are built from an address; execution takes the context.**
26
+ `PlasmaVault(vault)`, `ERC20(token)`, `AccessManager(manager)`; then
27
+ `.call(ctx)`, `.send(ctx)`, `.build_transaction(ctx)`, `.estimate_gas(ctx)`. See
28
+ [Context-free wrappers](#context-free-wrappers).
29
+ 2. **Methods that read the chain to answer are `read_*(ctx, ...)`**:
30
+ `vault.get_balance_fuses()` is `vault.read_balance_fuses(ctx)`,
31
+ `access_manager.owner()` is `read_owner(ctx)`. See
32
+ [Renamed methods](#renamed-methods).
33
+ 3. **Pin a block with `ctx = ctx.at_block(n)`.** Assigning `ctx.default_block`
34
+ raises `AttributeError`. See [Vault reads and validation](#vault-reads-and-validation).
35
+ 4. **The vault read is one snapshot.** `fetch_vault_data(ctx, vault, *, block=None,
36
+ lending=True, metadata=True)` takes the vault address and the chain from `ctx`;
37
+ `validate_vault_state(state)` and `build_vault_info_json(data, *,
38
+ resolve_name=None)` are pure. See [Vault reads and validation](#vault-reads-and-validation).
39
+ 5. **Moved and removed names.** The crosschain encoders left
40
+ `ipor_fusion.fuses.crosschain` (no shim); `IporFusionMarkets.RWA`,
41
+ `MORPHO_BLUE_ADDRESS`, the 3.x cache functions, `read_erc20_holdings`,
42
+ `ExternalStateExecutor.mark_nav` and `NavMark` are gone; several functions take
43
+ their options by keyword or renamed a parameter. See
44
+ [Moved, removed and reshaped](#moved-removed-and-reshaped).
45
+ 6. **Errors.** Every SDK exception derives from `IporFusionError`; some raises changed
46
+ class (empty call results, crosschain discovery, simulation, Morpho API). See
47
+ [Errors](#errors).
48
+ 7. **Cache and request limit.** `Web3Context.from_url` bounds requests in flight at
49
+ 8 (`max_concurrent_requests`; `InFlightLimit` to share a bound, `None` to lift
50
+ it), and the contract cache is injected per context (`NullCache()` keeps
51
+ nothing). See [Contract cache and request limit](#contract-cache-and-request-limit).
52
+ 8. **Typed callers** (pyright, mypy, Pylance): ids, addresses and amounts are
53
+ nominal types. Wrap literals and raw values, `ChainId(8453)`, `Period(0)`,
54
+ `Amount(n)`, `parse_fuse_address(...)`; values the SDK returns already carry
55
+ their type. See [Nominal types](#nominal-types).
56
+
57
+ Then read [Behavior changes](#behavior-changes): they apply to every caller,
58
+ type-checked or not.
59
+
60
+ Addresses a user types in (CLI arguments, config files, forms): normalize first,
61
+ then parse strictly, `parse_plasma_vault_address(Web3.to_checksum_address(raw.strip()))`.
62
+ The `parse_*` address parsers reject hex without `0x` and surrounding whitespace;
63
+ `Web3.to_checksum_address` accepts hex without `0x` but not whitespace.
334
64
 
335
65
  ## Context-free wrappers
336
66
 
@@ -346,7 +76,8 @@ placeholder wrapper.
346
76
  `LegacyPriceOracleMiddleware(oracle, Decimals(8))`, the crosschain executor,
347
77
  dispatcher and factory wrappers, the Chainlink CCIP wrappers, the readers
348
78
  (`MorphoReader(morpho)`, `AaveV3Reader(pool)`, ...). `HyperCoreReader()` takes no
349
- argument. The nominal address types are unchanged.
79
+ argument, and `HyperCorePendingReader(vault, reader)` takes the vault's
80
+ `PlasmaVaultAddress`, not a `PlasmaVault` wrapper.
350
81
  - Execute a `Call` on a context: `.call(ctx, *, block=None)` (reads default to the
351
82
  context's, possibly pinned, block), `.send(ctx)`, `.build_transaction(ctx, *,
352
83
  block="latest")`, `.estimate_gas(ctx, *, block="latest")`. `ctx` may be passed by
@@ -401,6 +132,16 @@ placeholder wrapper.
401
132
  | `call.send()` | `call.send(ctx)` |
402
133
  | `call.build_transaction(block=b)`, `call.estimate_gas(block=b)` | `call.build_transaction(ctx, block=b)`, `call.estimate_gas(ctx, block=b)` |
403
134
  | `call.ctx` | gone: keep the context yourself |
135
+ | `Multicall3(ctx, address)` | `Multicall3(ctx, address=address)` |
136
+ | `executor.pending_proposals(account)` | `executor.pending_proposals(account, chain_id=ctx.chain_id)` |
137
+ | `executor.parse_balance_proposed(log)` / `find_balance_proposed(logs)` | `...(log, chain_id=...)` / `...(logs, chain_id=...)` |
138
+ | `CcipLane(executor=, dispatcher=, spoke_chain_id=, ...)` | `CcipLane(hub_ctx=, spoke_ctx=, executor=, dispatcher=, ...)` (same for `StargateLane`) |
139
+ | `transport.executor(ctx, address)` / `transport.dispatcher(ctx, address)` | `transport.executor(address)` / `transport.dispatcher(address)` |
140
+
141
+ ### Renamed methods
142
+
143
+ | 3.x | 4.0 |
144
+ |---|---|
404
145
  | `PlasmaVault.get_balance_fuses()` | `PlasmaVault.read_balance_fuses(ctx)` |
405
146
  | `PlasmaVault.withdraw_manager_address()` | `PlasmaVault.read_withdraw_manager_address(ctx)` |
406
147
  | `PlasmaVault.price_oracle_address()` | `PlasmaVault.read_price_oracle_address(ctx)` |
@@ -417,13 +158,8 @@ placeholder wrapper.
417
158
  | `AaveV3Reader.position_breakdown(asset, user)` | `AaveV3Reader.read_position_breakdown(ctx, asset, user)` |
418
159
  | `AaveV3FuseReader.pool()` | `AaveV3FuseReader.read_pool(ctx)` |
419
160
  | `PlasmaVault.read(target, data)` / `read_as(target, data, ...)` | `PlasmaVault.universal_read(target, data)` / `universal_read_decoded(target, data, ...)` (they build a Call; `read_*` names only methods that execute) |
420
- | `read_index_of(asset)` | `precompile_index_of(asset)` |
161
+ | `read_index_of(asset)` | `precompile_index_of(asset)` (it reads nothing) |
421
162
  | `ctx.default_block = n` | `ctx = ctx.at_block(n)` |
422
- | `Multicall3(ctx, address)` | `Multicall3(ctx, address=address)` |
423
- | `executor.pending_proposals(account)` | `executor.pending_proposals(account, chain_id=ctx.chain_id)` |
424
- | `executor.parse_balance_proposed(log)` / `find_balance_proposed(logs)` | `...(log, chain_id=...)` / `...(logs, chain_id=...)` |
425
- | `CcipLane(executor=, dispatcher=, spoke_chain_id=, ...)` | `CcipLane(hub_ctx=, spoke_ctx=, executor=, dispatcher=, ...)` (same for `StargateLane`) |
426
- | `transport.executor(ctx, address)` / `transport.dispatcher(ctx, address)` | `transport.executor(address)` / `transport.dispatcher(address)` |
427
163
 
428
164
  A service that caches wrappers per chain can now share one wrapper across chains and
429
165
  snapshots; what it must not share is a `Call` result decoded with a chain-bound
@@ -431,105 +167,136 @@ argument (`chain_id=`) for another chain. An external signer still needs the rea
431
167
  target address (`call.to`) and builds the chain-qualified transaction itself, or
432
168
  gets it from `call.build_transaction(ctx)`.
433
169
 
434
- ## Contract cache
170
+ ## Vault reads and validation
435
171
 
436
- Contract names, token symbols and decimals and deployment blocks are cached in a
437
- `ContractCache`, injected instead of always written to `~/.cache`:
172
+ The vault read is one pinned snapshot, and the chain is always the context's:
438
173
 
439
- - `Web3Context(..., cache=...)` and `Web3Context.from_url(..., cache=...)` take one
440
- (keyword-only); `ctx.cache` returns it, and `ctx.at_block(...)` keeps it. The
441
- default (`None`) is `default_contract_cache()`, a `FileContractCache` at
442
- `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`~/.cache/...` when unset),
443
- resolved when used rather than at import. `NullCache()` keeps nothing;
444
- `FileContractCache(path)` moves the file; any object with `get(keys)` and
445
- `put(entries)` over `CacheKey(chain_id, address, field)` → `str` fits the
446
- protocol (for example a shared store in a service).
447
- - `resolve_token_symbol`, `resolve_token_decimals`, the snapshot's token metadata
448
- (`fetch_vault_data(metadata=True)`) and `fetch_deployment_info` use `ctx.cache`.
449
- `get_contract_name(chain_id, address, api_key=None, *, cache=None)` takes the cache
450
- explicitly (default: the file cache); pass `cache=ctx.cache` to share the
451
- context's policy. The CLI's text and JSON output name contracts through the
452
- context's cache.
453
- - `build_vault_info_json(data)` without `resolve_name` names no contract (every
454
- `contract` is `"?"`) instead of reading names from the on-disk cache; pass
455
- `resolve_name=partial(get_contract_name, chain_id, api_key=..., cache=ctx.cache)`
456
- for the CLI's document.
457
- - `ipor_fusion.cache` is a public module now and exports `ContractCache`,
458
- `FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
459
- `default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
460
- are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
461
- `load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
462
- `DEPLOYMENT_CACHE_FILE`. `cache_key(chain_id, address, field)` returns a `CacheKey`
463
- and raises `ValueError` for an address that is not 20 bytes of hex, so
464
- `get_contract_name` does too.
174
+ - `fetch_vault_data(ctx, vault, *, block=None, lending=True, metadata=True)` takes
175
+ the vault address (not a `PlasmaVault`) and no `chain_id`. It resolves one block
176
+ (`block`, else a numeric `ctx.default_block`, else the latest) and runs every
177
+ read, log scans included, on `ctx.at_block(block)`; `ctx` is not modified. In 3.x
178
+ `block_number` only selected the block reported in `VaultData`, and the reads ran
179
+ wherever `ctx` pointed. The old `chain_id=0` switch is `lending=False`;
180
+ `metadata=False` skips the display metadata: `token_symbols` and `token_decimals`
181
+ stay empty and the ERC20 holdings' symbols are `"?"`. The underlying asset's
182
+ symbol and decimals and the decimals the valuation needs are still read. With
183
+ `metadata=True` the holdings are named by the same cached, batched read as
184
+ `token_symbols`. `VaultData` gains `vault`, `chain_id`, `market_balances`,
185
+ `erc20`, `token_symbols` and `token_decimals`; `WithdrawManagerData` gains
186
+ `shares_to_release_assets` and `pending_request_assets`.
187
+ - `validate_vault(ctx, vault, *, block=None, metadata=False)` fetches (by default
188
+ without display metadata, so its ERC20 holdings are named `"?"`) and validates; it
189
+ no longer takes `resolve_symbol`. `metadata=True` names the ERC20 holdings in the
190
+ result and its findings. `validate_vault_state(state)` is pure: a custom
191
+ `VaultState` also provides `vault`, `chain_id`, `market_balances`, `erc20` and the
192
+ queue's `shares_to_release_assets`. `read_erc20_holdings` is gone: the holdings are
193
+ `VaultData.erc20`.
194
+ - `build_vault_info_json(data, *, resolve_name=None)` renders from the snapshot and
195
+ reads nothing; the chain and its label come from `data.chain_id`. Without
196
+ `resolve_name` it names no contract (see
197
+ [Contract cache and request limit](#contract-cache-and-request-limit)).
198
+ - `fetch_deployment_info(ctx, vault, *, api_key=None)` and
199
+ `fetch_vault_lending_health(ctx, vault, *, balance_fuse_market_ids,
200
+ market_substrates, aave_pools=None)` take the chain from `ctx`.
201
+ - `Web3Context.at_block(block)` returns a pinned copy (same connection, signer, log
202
+ source, cache and scan bounds). `Web3Context.default_block` is read-only:
203
+ `ctx.default_block = n` raises `AttributeError`; the old setter changed the block
204
+ under every thread sharing the context. `Web3Context.get_logs(to_block=None)` ends
205
+ at the context's default block and `Web3Context.get_block(block=None)` returns it,
206
+ both instead of `"latest"`, so a pinned context never sees later logs or blocks.
207
+ - `VaultData.price_oracle_addr` is renamed `price_oracle`, like its sibling address
208
+ fields (a custom `VaultState` renames it too).
465
209
 
466
- **File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
467
- `symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
468
- load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
469
- 3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
470
- discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
471
- is no longer read: a vault's deployment block is resolved again (it needs an
472
- Etherscan key) and the old file can be deleted.
210
+ ## Moved, removed and reshaped
473
211
 
474
- **Behavior.**
212
+ **Public API.** 4.0 declares it: `ipor_fusion.__all__`, which now also exports the
213
+ names earlier reachable only by module path (`FusionFactory`, `FusionInstance`,
214
+ `fetch_market`, `resolve_token_symbol`, the crosschain codecs, ...). Import from
215
+ `ipor_fusion`: a module path outside the README's list (*Public API and stability*)
216
+ still works but is internal. Every project type a public signature or field uses is
217
+ exported too, among them `AaveV3PositionBreakdown`, `AaveV3ReserveTokens`,
218
+ `AccountRequest`, `StateOverrideProvider`, `TransferDirection` and the validation
219
+ vocabulary `Severity`, `Verdict` and `Check`. Also new: `InFlightLimit` (see
220
+ [Contract cache and request limit](#contract-cache-and-request-limit)) and
221
+ `log_bytes`, the hex-or-bytes log field helper the crosschain decoders use.
475
222
 
476
- - The same address on two chains has two entries: a token's symbol or decimals read
477
- on one chain is never served for another.
478
- - Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
479
- address without code, but no longer stores it, so a token deployed later (or seen at
480
- a later block) resolves.
481
- - A write merges with the file as it is on disk and replaces it atomically (a
482
- temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
483
- processes writing at once never leave a torn file; one of two concurrent writers may
484
- lose its newest entry, which is read again later.
485
- - A cache never fails a read: a corrupt or unreadable file reads as empty (logged
486
- warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
487
- directory (read-only file system) keeps the entries in memory for the process; a
488
- custom cache that raises is treated as a miss, and so is a stored number that does
489
- not parse.
490
- - `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
491
- transaction or its block cannot be read; a cache failure is no longer reported as
492
- one (the deployment block is returned).
223
+ **Crosschain fuse encoders moved.** The crosschain market encoders moved from
224
+ `ipor_fusion.fuses.crosschain` into the crosschain package, and
225
+ `ipor_fusion.fuses` no longer re-exports them. There is no shim: the old paths
226
+ raise `ImportError`. The top-level names (`from ipor_fusion import ...`) are
227
+ unchanged.
493
228
 
494
- ## Behavior changes
229
+ | 3.x import | 4.0 import |
230
+ |---|---|
231
+ | `ipor_fusion.fuses.crosschain` | `ipor_fusion.crosschain.fuses` (or `ipor_fusion.crosschain`) |
232
+ | `ipor_fusion.fuses.crosschain.base`, `.ccip`, `.stargate` | `ipor_fusion.crosschain.fuses` (its submodules `base`, `ccip` and `stargate` exist but are internal) |
233
+ | `from ipor_fusion.fuses import CcipCrosschainSupplyFuse` (and the other crosschain encoders, `SendParams`, `CrosschainSubstrate*`, `crosschain_market_id`) | `from ipor_fusion.crosschain import ...` or `from ipor_fusion import ...` |
495
234
 
496
- - Two fuses are equal (and hash alike) only when they have the same class and the
497
- same stored state. `StakeFuse` subclasses (`GearboxStakeFuse`,
498
- `FluidInstadappStakingFuse`) with the same fuse address but different
499
- `staking_address` were equal and collapsed in a set or dict key; they are now
500
- distinct. A custom `Fuse` subclass is compared on its instance attributes, which
501
- must be hashable; drop an `__eq__` / `__hash__` override that only added them.
502
- - The MCP `vault_info` document reports the vault address checksummed (the CLI
503
- already did).
504
- - `SubstrateInfo.address` is `None` instead of `""` for a substrate without an
505
- address. Truthiness checks are unaffected and the SDK's JSON documents already
506
- omit it, but `dataclasses.asdict(info)` now gives `"address": None` and
507
- `info.address == ""` is false.
508
- - The Uniswap V3 / Ramses V2 position events (`extract_new_position_events`,
509
- `extract_close_position_events`) return their addresses checksummed (they were the
510
- lowercase ABI-decoded strings).
511
- - `fusion market meta-morpho --json` and the MCP `market_meta_morpho` tool report
512
- `null` instead of `""` for an absent curator, guardian or fee recipient; the MCP
513
- schema's `curator` is nullable.
514
- - The Morpho API client checksums every address it returns (market, vault,
515
- allocation, adapter and cap records, and the `fusion market` / MCP market documents
516
- built from them), so string comparisons against the API's own spelling change.
517
- - It raises `MorphoApiError` for a market id or address it parses (market and V1
518
- allocation ids, V1 flow-cap ids, vault/asset/owner/token/oracle/IRM/allocator
519
- addresses) when the value is invalid, or absent where the field is optional in the
520
- answer. Any other answer it cannot read (a missing key such as a `loanAsset`
521
- without `address`, a value of the wrong type, a body that is not a JSON object) and
522
- any transport failure also raise `MorphoApiError`, never `KeyError`, `TypeError` or
523
- `ValueError` (see [Errors](#errors)). When matching a market's own allocation and flow cap, an id that
524
- does not parse matches nothing, and a V1 flow cap without an id is skipped, as
525
- before. `--market` rejects a malformed id.
526
- - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
527
- documents already were).
528
- - `TransactionError.tx_hash` is `0x`-prefixed.
529
- - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
530
- a value that is not an address.
531
- - The contract cache keys every entry by chain and lowercase address and no longer
532
- caches an absence (see [Contract cache](#contract-cache)).
235
+ `ipor_fusion.crosschain` now exports the whole crosschain surface: besides the
236
+ codecs and wrappers, the encoders, `CrosschainLane`, `CcipLane`, `StargateLane`,
237
+ the transports and chains, `discover_deployment`, `open_lane(s)`, `LANES` and
238
+ `CrosschainSimulator`, which 3.x exported only from `ipor_fusion`.
239
+ `ipor_fusion.crosschain.ccip` and `.stargate` keep exporting only their codecs
240
+ and wrappers.
241
+
242
+ **Fuse base.** `Fuse`, `StakeFuse` and the shared validators and substrate packers
243
+ moved to `ipor_fusion.core.fuse`. Import `Fuse` and `StakeFuse` from `ipor_fusion`
244
+ or `ipor_fusion.fuses`. `ipor_fusion.fuses.base` still re-exports them in 4.0, but
245
+ it is a submodule of a public subpackage and therefore internal (see the README,
246
+ *Public API and stability*); so are the underscore validators and packers.
247
+ `FuseAction` lives in `ipor_fusion.core.fuse_action` and `ZERO_ADDRESS` in
248
+ `ipor_fusion.types`; import both from `ipor_fusion` (`FuseAction` also from
249
+ `ipor_fusion.fuses`, `ZERO_ADDRESS` from the public module `ipor_fusion.types`).
250
+ The internal `ipor_fusion.fuses.base` still re-exports them. The CLI's internal
251
+ `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
252
+ `vault_markets_view` and `oracle_mapping_view` (private names, not API).
253
+
254
+ **Removed.**
255
+
256
+ - `IporFusionMarkets.RWA`: market 50 is `IporFusionMarkets.EXTERNAL_STATE`.
257
+ - `MORPHO_BLUE_ADDRESS`: use `morpho_blue_address(chain_id)`. `BLOCK_EXPLORER_URLS`,
258
+ `MORPHO_BLUE_ADDRESSES`, `AAVE_V3_POOL` and `explorer.SUPPORTED_CHAINS` are
259
+ internal. Import `CHAIN_NAMES`, `CHAIN_NAME_TO_ID`, `SUPPORTED_CHAIN_IDS`,
260
+ `GET_LOGS_RANGE_HINTS`, `PUBLIC_ALLOCATOR_ADDRESSES`, `HYPERCORE_PENDING_READERS`,
261
+ `HYPEREVM_CHAIN_ID` and `HYPERCORE_MARKET` from `ipor_fusion` (the HyperCore ones
262
+ also from `ipor_fusion.readers`).
263
+ - `read_erc20_holdings`: the holdings are `VaultData.erc20`.
264
+ - `ExternalStateExecutor.mark_nav` and `NavMark`: send `propose_balance` and
265
+ `confirm_balance` from separate custodians, then
266
+ `PlasmaVault.update_markets_balances([IporFusionMarkets.EXTERNAL_STATE])`.
267
+ - The 3.x cache functions and constants (`load_contract_cache`, ...): see
268
+ [Contract cache and request limit](#contract-cache-and-request-limit).
269
+ - `ContractWrapper.encoder()` and `Call.ctx`: see
270
+ [Context-free wrappers](#context-free-wrappers).
271
+
272
+ **Options by keyword and renamed parameters.** A positional call to one of these
273
+ fails at run time:
274
+
275
+ - `Web3Context(web3, chain_id, *, ...)` and `Web3Context.from_url(url, *, ...)` take
276
+ their options by keyword.
277
+ - `VaultSimulator(web3, *, vault=None, alpha=None, block=...)` takes every argument
278
+ after `web3` by keyword, and `vault` and `alpha` are optional: a simulator that only
279
+ observes, deploys or sends `execute_call(call, from_=...)` needs neither, so drop a
280
+ zero-address placeholder. `execute()` without `vault` and `alpha` raises
281
+ `ValueError`, as does `execute_call` without `from_` or `alpha`.
282
+ - `fetch_unpriceable_priced_tokens(ctx, *, price_oracle, underlying, balance_fuses,
283
+ market_substrates, morpho_positions)` takes everything after `ctx` by keyword.
284
+ - `get_contract_name(chain_id, address, *, api_key=None, cache=None)` and
285
+ `get_deployment_tx(chain_id, address, *, api_key=None)` take `api_key` by keyword.
286
+ - `build_morpho_blue_market_json(*, market_id, chain_id, params, state, rates,
287
+ api_market=None, api_error=None, public_allocator=None)` takes every argument by
288
+ keyword.
289
+ - `encode_oft_compose_msg(*, nonce=, src_eid=, amount_ld=, compose_msg=)` takes
290
+ keyword arguments only.
291
+ - The vault parameter is named `vault` throughout: `validate_vault`,
292
+ `fetch_deployment_info`, `fetch_vault_lending_health`, `build_oracle_mapping`,
293
+ `read_external_state`, `ExternalStateExecutor.for_vault`, `read_hypercore_nav`,
294
+ `read_hypercore_vault_state`, `read_hypercore_transfer_snapshot`,
295
+ `read_hypercore_evm_exit_ceiling` and `resolve_access_manager` renamed
296
+ `vault_address` to `vault`; only a call that passes it by keyword changes.
297
+ - `FusionInstance.from_dict(data)` rebuilds a stored instance (the
298
+ `dataclasses.asdict` form) with every field parsed; use it instead of
299
+ `FusionInstance(**data)`, which passes raw strings into the role-typed fields.
533
300
 
534
301
  ## Errors
535
302
 
@@ -559,7 +326,7 @@ is the wrong one.
559
326
  - `AccessManager.read_owner(ctx)` raises the new `OwnerNotFoundError` (a
560
327
  `LookupError`) when no account holds `OWNER_ROLE`, instead of an `IndexError`.
561
328
  - `EmptyCallResultError` is no longer an eth_abi `InsufficientDataBytes` (nor a
562
- `DecodingError`). Code that caught either around `.call()` to tolerate a missing
329
+ `DecodingError`). Code that caught either around `.call(ctx)` to tolerate a missing
563
330
  contract or getter must catch `EmptyCallResultError`; return data that is present
564
331
  but too short still raises eth_abi's error.
565
332
  - Crosschain discovery (`discover_deployment`, `discover_lane_fuses`,
@@ -576,9 +343,20 @@ is the wrong one.
576
343
  - `VaultSimulator.run` (provider error from `eth_simulateV1`, or a result count that
577
344
  does not match the queued calls) and `CrosschainSimulator.relay` (not settled within
578
345
  `max_rounds`) raise the new `SimulationRunError`, still a `RuntimeError`.
579
- - The Morpho API client raises `MorphoApiError` for every response it cannot read and
580
- for any `OSError` or `http.client.HTTPException` while fetching (it caught only
581
- `URLError` and `TimeoutError` before).
346
+ - The Morpho API client raises `MorphoApiError` for every answer it cannot use and
347
+ every transport failure, never `KeyError`, `TypeError` or `ValueError`:
348
+ - a required market id or address (market and V1 allocation ids, V1 flow-cap ids,
349
+ vault/asset/owner/token/oracle/IRM/allocator addresses) that is absent or
350
+ invalid; an optional address (curator, guardian, fee recipients, ...) that is
351
+ absent or `""` reads as `None`, and raises only when present and invalid;
352
+ - any other answer it cannot read (a missing key such as a `loanAsset` without
353
+ `address`, a value of the wrong type, a body that is not a JSON object);
354
+ - any `OSError` or `http.client.HTTPException` while fetching (it caught only
355
+ `URLError` and `TimeoutError` before).
356
+
357
+ When matching a market's own allocation and flow cap, an id that does not parse
358
+ matches nothing, and a V1 flow cap without an id is skipped, as before.
359
+ `fusion market meta-morpho --market` rejects a malformed id.
582
360
  - The MCP `market_meta_morpho` tool lets `MorphoApiError` propagate instead of
583
361
  re-raising it as `ValueError`. MCP clients see the same tool error message; only a
584
362
  direct Python call of the tool function sees the new type.
@@ -614,3 +392,337 @@ is the wrong one.
614
392
  - `quote_ccip_native_fee` raises `SimulationError` (label `"quote"`, with
615
393
  `revert_reason`) instead of `ValueError` when the call reverts before the CCIP fee
616
394
  check. A call that succeeds without sending a message is still a `ValueError`.
395
+ - `cache_key(chain_id, address, field)` raises `ValueError` for an address that is
396
+ not 20 bytes of hex, so `get_contract_name` does too.
397
+
398
+ The rule covers what the SDK itself raises. Errors from web3, the provider or the
399
+ HTTP transport while `Call.call(ctx)`, `.send(ctx)` or a `Web3Context` method runs
400
+ are not wrapped in an `IporFusionError`; they propagate as web3 raises them (a log
401
+ scan that cannot complete raises `LogScanError` instead of returning partial
402
+ logs).
403
+
404
+ ## Contract cache and request limit
405
+
406
+ **Request limit.** `Web3Context.from_url(..., max_concurrent_requests=8)` bounds the
407
+ RPC requests in flight across every thread using the context (`None` lifts the
408
+ bound); the vault reads fan out on nested thread pools, and the bound is what a
409
+ rate-limited provider sees. An `int` bounds one context. To bound several contexts on
410
+ one provider (a service that builds a context per request or per scan), create one
411
+ `InFlightLimit(n)` per provider and pass it as `max_concurrent_requests=`: every
412
+ context built with it shares the same `n` slots. A context built from your own
413
+ `Web3` object is not bounded unless you add the limit to it: `limit.install(web3)`
414
+ (innermost, as `from_url` does) or
415
+ `web3.middleware_onion.inject(limit.middleware, layer=0)`.
416
+
417
+ **Contract cache.** Contract names, token symbols and decimals and deployment blocks
418
+ are cached in a `ContractCache`, injected instead of always written to `~/.cache`:
419
+
420
+ - `Web3Context(..., cache=...)` and `Web3Context.from_url(..., cache=...)` take one
421
+ (keyword-only); `ctx.cache` returns it, and `ctx.at_block(...)` keeps it. The
422
+ default (`None`) is `default_contract_cache()`, a `FileContractCache` at
423
+ `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`~/.cache/...` when unset),
424
+ resolved when used rather than at import. `NullCache()` keeps nothing;
425
+ `FileContractCache(path)` moves the file; any object with `get(keys)` and
426
+ `put(entries)` over `CacheKey(chain_id, address, field)` → `str` fits the
427
+ protocol (for example a shared store in a service).
428
+ - `resolve_token_symbol`, `resolve_token_decimals`, the snapshot's token metadata
429
+ (`fetch_vault_data(metadata=True)`) and `fetch_deployment_info` use `ctx.cache`.
430
+ `get_contract_name(chain_id, address, *, api_key=None, cache=None)` takes the cache
431
+ explicitly (default: the file cache); pass `cache=ctx.cache` to share the
432
+ context's policy. The CLI's text and JSON output name contracts through the
433
+ context's cache.
434
+ - `build_vault_info_json(data)` without `resolve_name` names no contract (every
435
+ `contract` is `"?"`) instead of reading names from the on-disk cache; pass
436
+ `resolve_name=partial(get_contract_name, chain_id, api_key=..., cache=ctx.cache)`
437
+ for the CLI's document.
438
+ - `ipor_fusion.cache` is a public module now and exports `ContractCache`,
439
+ `FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
440
+ `default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
441
+ are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
442
+ `load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
443
+ `DEPLOYMENT_CACHE_FILE`.
444
+
445
+ **File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
446
+ `symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
447
+ load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
448
+ 3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
449
+ discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
450
+ is no longer read: a vault's deployment block is resolved again (it needs an
451
+ Etherscan key) and the old file can be deleted.
452
+
453
+ **Cache behavior.**
454
+
455
+ - The same address on two chains has two entries: a token's symbol or decimals read
456
+ on one chain is never served for another.
457
+ - Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
458
+ address without code, but no longer stores it, so a token deployed later (or seen at
459
+ a later block) resolves.
460
+ - A write merges with the file as it is on disk and replaces it atomically (a
461
+ temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
462
+ processes writing at once never leave a torn file; one of two concurrent writers may
463
+ lose its newest entry, which is read again later.
464
+ - A cache never fails a read: a corrupt or unreadable file reads as empty (logged
465
+ warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
466
+ directory (read-only file system) keeps the entries in memory for the process; a
467
+ custom cache that raises is treated as a miss, and so is a stored number that does
468
+ not parse.
469
+ - `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
470
+ transaction or its block cannot be read; a cache failure is no longer reported as
471
+ one (the deployment block is returned).
472
+
473
+ ## Nominal types
474
+
475
+ 4.0 gives chain, market and role ids, the crosschain, HyperCore and Morpho ids,
476
+ vault/oracle/token addresses, amounts and transaction hashes a nominal type
477
+ (`typing.NewType`) across the public API, and checks the SDK, the examples and a
478
+ typed client with pyright's `reportArgumentType`. Other fields keep their types
479
+ (e.g. symbols, names and URLs stay `str`). The wrappers do not change values:
480
+ `ChainId(1)` is `1` at runtime, so untyped code needs none of this section. Values
481
+ the SDK returns already carry their type and go back in unwrapped.
482
+
483
+ The SDK's own types are importable from `ipor_fusion` (and the public module
484
+ `ipor_fusion.types`), as are `USDC_TOKEN_INDEX` and `hip3_action_asset`. Third-party
485
+ types are not re-exported: `ChecksumAddress` and `HexAddress` come from `eth_typing`,
486
+ and `BlockIdentifier`, `Timestamp` and `TxParams` from `web3.types`. The wrappers
487
+ label values; they do not validate byte widths or address spelling.
488
+
489
+ ### Building the types
490
+
491
+ | Type | From a literal or raw value |
492
+ |---|---|
493
+ | `ChainId` | `ChainId(1)` |
494
+ | `MarketId` | an `IporFusionMarkets` member, or `MarketId(n)` |
495
+ | `RoleId` | a `Roles` member (accepted as is), or `RoleId(n)`; `Roles.resolve(name)` returns one |
496
+ | `Decimals`, `Period`, `Amount`, `Shares`, ... | `Decimals(8)`, `Period(0)`, `Amount(n)`, `Shares(n)` |
497
+ | `ChecksumAddress` | `Web3.to_checksum_address(value)` |
498
+ | `HexAddress` | any `ChecksumAddress`, or `parse_hex_address(value)` (20 bytes or `0x`/`0X`-prefixed hex in any case → lowercase `0x` form) |
499
+ | `MorphoBlueMarketId` | `parse_morpho_market_id(value)`: 32 bytes or hex in any spelling, always `0x` + 64 lowercase digits |
500
+ | `TxHash` | `parse_tx_hash(value)` (32 bytes or `0x`/`0X`-prefixed hex in any case) |
501
+ | `PlasmaVaultAddress`, `FuseAddress`, `AccessManagerAddress` | from the SDK (`PlasmaVault.address`, `get_fuses()`, `get_access_manager_address()`, `FusionInstance`, discovery), or `parse_plasma_vault_address` / `parse_fuse_address` / `parse_access_manager_address(value)` for a raw address; they label the role and check nothing on chain |
502
+ | crosschain ids | `CommandId(b)`, `MessageId(b)`, `AssetId(b)`, `OperationId(b)`, `TransferId(b)`, `ReturnRequestId(b)`, `PositionSetHash(b)` (bytes32); `ProposalId(n)`, `DeploymentRequestId(n)`, `StargateAssetId(n)`, `LayerZeroEid(n)`, `CcipChainSelector(n)` (int) |
503
+ | HyperCore ids | `HyperCoreTokenIndex(n)` (or `USDC_TOKEN_INDEX`), `HyperCoreAsset(n)` (or `hip3_action_asset(dex, index)`), `HyperCoreOrderId(n)`, `HyperCoreClientOrderId(n)`, `MarginTableId(n)`, `HyperCoreActionId(n)`, `PayloadHash(b)` |
504
+ | external state | `ProposalHash(b)` |
505
+ | `BlockIdentifier` | a block number, `"latest"` / `"pending"` / ..., or a block hash |
506
+
507
+ The address and hash parsers (`parse_hex_address`, `parse_tx_hash`,
508
+ `parse_plasma_vault_address`, `parse_fuse_address`, `parse_access_manager_address`)
509
+ validate strictly: a string needs exactly one `0x`/`0X` prefix and the exact number of
510
+ hex digits, with no surrounding whitespace; anything else raises `ValueError`.
511
+ Normalize user input first (see [Start here](#start-here)).
512
+ `parse_morpho_market_id` still accepts hex without `0x`.
513
+
514
+ `MessageId` covers both a CCIP `messageId` and a LayerZero `guid`. `AssetId` is the
515
+ crosschain executor's `bytes32` asset id; a Stargate pool's `uint16` asset id is
516
+ `StargateAssetId`. `DeploymentRequestId` is a factory's `uint256` dispatcher request;
517
+ a `bytes32` return (recall) request is `ReturnRequestId`.
518
+
519
+ ### Typed signatures, by area
520
+
521
+ **Units.** `VaultData.total_assets`, `underlying_balance_on_vault` and
522
+ `market_balances` values are `Amount`; `total_supply` and `supply_cap` are `Shares`;
523
+ `share_decimals` / `asset_decimals` are `Decimals`; `block_timestamp` and
524
+ `deployment_timestamp` are `Timestamp`. `FeeData` fee fields are `Fee`,
525
+ `management_fee_last_update` a `Timestamp`, `unrealized_management_fee` an `Amount`;
526
+ `WithdrawManagerData.withdraw_window` is a `Period`, its fees `Fee`,
527
+ `shares_to_release` `Shares`, `last_release_funds_timestamp` a `Timestamp` and the
528
+ converted amounts `Amount`. A custom `VaultState` annotates the same.
529
+ `VaultValidation.block_timestamp` is a `Timestamp` and `asset_decimals` `Decimals`;
530
+ `Erc20Holding.decimals` / `balance` are `Decimals | None` / `Amount | None` and
531
+ `Erc20Holdings.value_in_asset` / `market_cached_value` `Amount`;
532
+ `VaultData.token_decimals` values and `resolve_token_decimals` are `Decimals | None`.
533
+ `PlasmaVault.total_supply()`, `balance_of()` and `get_total_supply_cap()` return
534
+ `Shares` (vault shares), and `transfer` / `approve` / `transfer_from` /
535
+ `set_total_supply_cap` take `Shares`; `ERC20` keeps `Amount`.
536
+
537
+ **Vault data and helpers.** `ensure_supported_chain` takes `ChainId`.
538
+ `fetch_unpriceable_priced_tokens` takes the oracle and underlying as
539
+ `ChecksumAddress`. `resolve_token_symbol`, `resolve_token_decimals`,
540
+ `get_contract_name` and `get_deployment_tx` take `HexAddress` (a checksum address
541
+ fits), and the `resolve_name` / `resolve_symbol` callbacks receive one (a callback
542
+ typed for `str` still fits). Market-keyed data is keyed by `MarketId`:
543
+ `VaultData.market_substrates`, `dependency_graph`, `morpho_positions`,
544
+ `aave_positions`, `market_balances`; the `VaultState` protocol,
545
+ `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens` and the
546
+ `dependency_graph` helpers (`compute_update_reach`, `compute_update_groups`,
547
+ `find_orphan_fuse_markets`, `find_markets_missing_erc20_dependency`).
548
+ `find_orphan_fuse_markets` returns `dict[MarketId, list[FuseAddress]]`, so its fuses
549
+ go into `PlasmaVault.remove_fuses` as they are.
550
+
551
+ `VaultData.asset`, `price_oracle`, `rewards_manager`, `withdraw_manager`,
552
+ `FeeData.fee_manager` / `ipor_dao_fee_recipient` and `VaultValidation.vault` /
553
+ `asset` are `ChecksumAddress` (`VaultValidation.vault` a `PlasmaVaultAddress`);
554
+ `VaultData.access_manager` is an `AccessManagerAddress`, `fuses` / `instant_fuses`
555
+ and the `fuse_markets` keys are `FuseAddress`, `balance_fuses` is
556
+ `list[BalanceFuse]`, and `token_prices_usd` is keyed by lowercase `HexAddress`. A
557
+ custom `VaultState` implementation annotates `asset` and `price_oracle` as
558
+ `ChecksumAddress`, `fuse_markets` keys as `FuseAddress`, and its
559
+ `unpriceable_priced_tokens` items' `market_id` as `MarketId` (`UnpriceableToken`).
560
+ The Uniswap V3 / Ramses V2 position events' `version`, `sender` and `recipient` are
561
+ `ChecksumAddress`. `VaultValidation.chain_id` is `ChainId`;
562
+ `LendingMarketHealth.market_id` and `MiddlewarePricedToken.market_id` are
563
+ `MarketId`; `Erc20Holding.address` is `HexAddress`, and `SubstrateInfo.address` is
564
+ `HexAddress | None`. `LegacyPriceOracleMiddleware` takes `base_currency_decimals:
565
+ Decimals`, and `PriceOracleMiddleware.base_currency_decimals()` returns
566
+ `Call[Decimals]`. `get_logs_adaptive(chain_id=)` and `LogSource.get_logs` take
567
+ `ChainId`; `quote_ccip_native_fee(block=)` and
568
+ `OracleMappingReader.asset_source_events` take a `BlockIdentifier`.
569
+
570
+ **Address roles.** `PlasmaVault(address)`,
571
+ `validate_vault`, `fetch_deployment_info`, the HyperCore, lending-health and
572
+ oracle-mapping readers, `read_external_state`, `VaultSimulator(vault=)`,
573
+ `CrosschainSimulator`, crosschain discovery, the hub-manager inputs
574
+ (`ExecutorInitParams.manager`, `create_executor(manager=)`, `executors_of(manager=)`),
575
+ the async command helpers `Command.request_shares` / `redeem_from_request`,
576
+ `pending_request_shares` and `WithdrawManager.update_plasma_vault_address` take a
577
+ `PlasmaVaultAddress`; `.address`, `FusionInstance.plasma_vault` and the executor's
578
+ `manager()` return one. Crosschain deposit and redeem destinations stay
579
+ `ChecksumAddress`: `Command.deposit` / `redeem`, `is_allowed_vault`,
580
+ `is_tracked_vault` and the remote-vault substrate accept any allowlisted ERC4626.
581
+ Every fuse constructor, `FuseAction.fuse`, `BalanceFuse.fuse`,
582
+ `PlasmaVault.add_fuses` / `remove_fuses` / `add_balance_fuse` / `remove_balance_fuse`
583
+ / `balance_fuse_value`, `RewardsManager.add_reward_fuses` / `is_reward_fuse_supported`,
584
+ `LaneFuses`, the HyperCore balance-fuse argument and the
585
+ `PlasmaVault.configure_instant_withdrawal_fuses` configs
586
+ (`Sequence[tuple[FuseAddress, Sequence[bytes]]]`) take a `FuseAddress`, and
587
+ `get_fuses()`, `get_instant_withdrawal_fuses()`, `get_rewards_fuses()` and
588
+ `VaultData.fuses` / `instant_fuses` / `fuse_markets` keys return it.
589
+ `AccessManager(address)` takes an `AccessManagerAddress`;
590
+ `get_access_manager_address()`, `VaultData.access_manager` and
591
+ `FusionInstance.access_manager` return one. `resolve_access_manager` still takes any
592
+ `ChecksumAddress`: it is what checks that the address is a vault.
593
+
594
+ **Chain registry.** `CHAIN_NAMES`, `CHAIN_NAME_TO_ID`, `SUPPORTED_CHAIN_IDS`,
595
+ `GET_LOGS_RANGE_HINTS`, `BLOCK_EXPLORER_URLS`, `MORPHO_BLUE_ADDRESSES`,
596
+ `AAVE_V3_POOL`, `PUBLIC_ALLOCATOR_ADDRESSES` and `HYPERCORE_PENDING_READERS` are keyed
597
+ by `ChainId` (`HYPEREVM_CHAIN_ID` is one) and `explorer.SUPPORTED_CHAINS` is a
598
+ `frozenset[ChainId]`, so `ensure_supported_chain(CHAIN_NAME_TO_ID["ethereum"])`
599
+ type-checks; `PUBLIC_ALLOCATOR_ADDRESSES` values are `ChecksumAddress`.
600
+
601
+ **Markets, substrates and access.** Every `IporFusionMarkets` member is a `MarketId`.
602
+ `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
603
+ `AccessManager.has_role`, `grant_role`, `revoke_role` and `read_accounts_with_role`
604
+ take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
605
+
606
+ **Morpho.** `MorphoBlueMarketId` always carries `0x` (see
607
+ [Behavior changes](#behavior-changes)); build it with `parse_morpho_market_id`
608
+ instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market` and
609
+ `build_morpho_blue_market_json` take it; `MorphoApiMarket.market_id`,
610
+ `VaultV1MarketAllocation.market_id`, `VaultV2Cap.market_id`,
611
+ `MorphoPositionBreakdown.market_id` (as read from substrates) and the keys of
612
+ `VaultV1Info.public_allocator_flow_caps` are canonical. `fetch_market`, `fetch_vault`,
613
+ `morpho_blue_address` and the `build_*_json` helpers take `ChainId`; `fetch_vault`
614
+ takes a `ChecksumAddress`, and `build_morpho_blue_market_json` takes
615
+ `public_allocator` as `ChecksumAddress | None`. Every address on the `morpho_api`
616
+ records is `ChecksumAddress`; optional ones (`curator`, `guardian`, `fee_recipient`,
617
+ `performance_fee_recipient`, `management_fee_recipient`, `inner_vault`,
618
+ `VaultFlowCap.admin`, the `VaultV2Cap` market params) are `ChecksumAddress | None`.
619
+
620
+ **Crosschain.** The executor, dispatcher and factory wrappers, the lanes and
621
+ `StargateCrosschainCommandFuse.enter(request_id=)` take the crosschain ids above, and
622
+ their getters return them (`active_command`, `active_proposal_id`, `propose_balance`
623
+ on CCIP, `active_return` → `OperationId`, `asset_id`, `tracked_position_set_hash`,
624
+ `pending_request_by_chain`, `CrosschainLane.proposal_id_from_logs` /
625
+ `proposal_id_from_receipt`). Records carry them too: `Command`, `ActiveCommand`,
626
+ `ExecutorInitParams`, `BalanceObservation`, `LaneObservation`, `Observation`,
627
+ `CcipObservation`, `OutboundMessage`, `CcipMessageSent`, `Any2EVMMessage`,
628
+ `VerifierResult`, `Packet.guid`, `TaxiMessage`, `StargateChain`; and the helpers
629
+ `fetch_verifier_result(message_id=)`, `lz_receive_calldata(guid=)` and
630
+ `lz_compose_calldata(guid=)` take a `MessageId`. Decoded event values
631
+ (`ccip_events` → `CcipEvent.values`) stay a generic mapping: convert a value
632
+ before passing it on.
633
+
634
+ **Transactions, LayerZero and CCIP.** `Web3Context.build_transaction` and
635
+ `Call.build_transaction` return web3's `TxParams` (the same keys; `data` is `0x` hex,
636
+ the fee fields are `Wei`). LayerZero endpoint ids are `LayerZeroEid`:
637
+ `StargateChain.eid`, `Packet.src_eid` / `dst_eid`, `EnforcedOptionParam.eid`,
638
+ `executor_eid()` and `eid_of()` return it and `chain_id_of`, `peers`,
639
+ `lz_receive_calldata(src_eid=)`, `encode_oft_compose_msg(src_eid=)` and the Stargate
640
+ fuse's `eid=` take it (`LayerZeroEid(30101)`). CCIP chain selectors are
641
+ `CcipChainSelector`: `CcipChain.chain_selector`, `CcipRouteConfig.chain_selector`,
642
+ `CcipTokenLane.chain_selector`, the `MessageV1`, `CcipMessageSent` and
643
+ `Any2EVMMessage` selectors and `CcipRouter.get_off_ramps()` carry it, and
644
+ `chain_id_of_selector`, `CcipRouter.is_chain_supported` / `get_on_ramp`,
645
+ `CcipTokenPool.is_supported_chain` and `ccip_token_lane` take it
646
+ (`CcipChainSelector(4949039107694359620)`). `ZERO_ADDRESS` is a `ChecksumAddress`
647
+ (same value).
648
+
649
+ **HyperCore (preview).** The fuses, `HyperCoreSubstrates`, `HyperCoreReader`,
650
+ `read_hypercore_transfer_snapshot` and the helpers `system_address`,
651
+ `precompile_index_of`, `perp_dex_of`, `is_native_perp_asset`, `is_hip3_asset` take the
652
+ HyperCore ids; `HyperCorePerpAssetInfo`, `HyperCorePerpMarket`, `HyperCoreSpotLeg`,
653
+ `HyperCorePendingState` and `HyperCoreTransferSnapshot` carry them.
654
+
655
+ **External state.** `ExternalStateExecutor.proposal_hash` returns and `confirm_balance`
656
+ takes a `ProposalHash`; `BalanceProposal.proposal_hash` is a `ProposalHash`;
657
+ `ExternalStateEvent.transaction_hash` is a `TxHash`.
658
+ `ExternalStateVaultState.last_checked_custodian_timestamp`,
659
+ `BalanceFuseSnapshot.last_custodian_update_timestamp`, `BalanceProposal.proposed_at`,
660
+ `last_updated()` and `get_oldest_update_timestamp()` are web3's `Timestamp`, and
661
+ `ExternalStateExecutor.proposal_hash(proposed_at=)` takes one.
662
+
663
+ **Errors and explorer.** `TransactionError(tx_hash=)` takes a `TxHash`,
664
+ `LogScanError(address=)` a `ChecksumAddress`; `get_revert_reason` takes a `TxHash`;
665
+ `get_deployment_tx` returns a `TxHash` (a malformed hash from Etherscan is reported
666
+ as the error `malformed-tx-hash`).
667
+
668
+ **MCP response models** (Python constructors only; the JSON schemas are unchanged
669
+ except as listed under [Behavior changes](#behavior-changes)): `chain_id` fields are
670
+ `ChainId`, `role_id` `RoleId`, `market_id` `MarketId`. Calling an MCP tool function
671
+ directly from Python takes `ChainId`.
672
+
673
+ ## Behavior changes
674
+
675
+ These apply to every caller, type-checked or not. Changes described in full above
676
+ are listed with a link.
677
+
678
+ - **Vault reads are pinned to one block**: every read of `fetch_vault_data`, log
679
+ scans included, runs at the resolved block; see
680
+ [Vault reads and validation](#vault-reads-and-validation).
681
+ - **`Web3Context.get_logs` and `get_block` default to the context's block**, not
682
+ `"latest"`; see [Vault reads and validation](#vault-reads-and-validation).
683
+ - **`metadata=False`** leaves `token_symbols` / `token_decimals` empty and names the
684
+ ERC20 holdings `"?"`, and `validate_vault` defaults to it; see
685
+ [Vault reads and validation](#vault-reads-and-validation).
686
+ - **`Web3Context.from_url` bounds requests in flight at 8** by default; see
687
+ [Contract cache and request limit](#contract-cache-and-request-limit).
688
+ - **The contract cache** keys every entry by chain and lowercase address, no longer
689
+ caches an absence, and is injected per context; see
690
+ [Contract cache and request limit](#contract-cache-and-request-limit).
691
+ - **The supply cap is formatted in share decimals.** `fusion vault info` shows it
692
+ labelled `shares`, and the JSON `supply_cap.formatted` uses `share_decimals`; both
693
+ used the asset's decimals before.
694
+ - **`"no contract"` no longer appears in vault output.** The snapshot reads token
695
+ symbols in one batch, which does not tell a token without code from one whose
696
+ symbol cannot be read: the text of `fusion vault info` / `validate` and the ERC20
697
+ holdings show it as `"?"`, and the `vault info` JSON (CLI and MCP) leaves its
698
+ symbol out or `null`. `resolve_token_symbol` itself still returns `"no contract"`.
699
+ - **Morpho market ids always carry `0x`.** `fetch_market` and
700
+ `build_morpho_blue_market_json` no longer add the prefix themselves: pass
701
+ `parse_morpho_market_id(raw_id)`, never a bare 64-hex string.
702
+ - The Morpho API client checksums every address it returns (market, vault,
703
+ allocation, adapter and cap records, and the `fusion market` / MCP market documents
704
+ built from them), so string comparisons against the API's own spelling change. Its
705
+ error handling is under [Errors](#errors).
706
+ - `fusion market meta-morpho --json` and the MCP `market_meta_morpho` tool report
707
+ `null` instead of `""` for an absent curator, guardian or fee recipient; the MCP
708
+ schema's `curator` is nullable.
709
+ - Two fuses are equal (and hash alike) only when they have the same class and the
710
+ same stored state. `StakeFuse` subclasses (`GearboxStakeFuse`,
711
+ `FluidInstadappStakingFuse`) with the same fuse address but different
712
+ `staking_address` were equal and collapsed in a set or dict key; they are now
713
+ distinct. A custom `Fuse` subclass is compared on its instance attributes, which
714
+ must be hashable; drop an `__eq__` / `__hash__` override that only added them.
715
+ - `SubstrateInfo.address` is `None` instead of `""` for a substrate without an
716
+ address. Truthiness checks are unaffected and the SDK's JSON documents already
717
+ omit it, but `dataclasses.asdict(info)` now gives `"address": None` and
718
+ `info.address == ""` is false.
719
+ - The MCP `vault_info` document reports the vault address checksummed (the CLI
720
+ already did).
721
+ - The Uniswap V3 / Ramses V2 position events (`extract_new_position_events`,
722
+ `extract_close_position_events`) return their addresses checksummed (they were the
723
+ lowercase ABI-decoded strings).
724
+ - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
725
+ documents already were).
726
+ - `TransactionError.tx_hash` is `0x`-prefixed.
727
+ - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
728
+ a value that is not an address.