ipor-fusion 4.0.0.dev45__tar.gz → 4.0.0.dev52__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 (287) hide show
  1. ipor_fusion-4.0.0.dev52/MIGRATION_4.md +166 -0
  2. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/PKG-INFO +5 -1
  3. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/README.md +4 -0
  4. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/pyproject.toml +2 -1
  5. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/__init__.py +4 -0
  6. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/vault_markets_view.py +6 -4
  7. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/context.py +3 -3
  8. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/dependency_graph.py +3 -1
  9. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/explorer.py +14 -4
  10. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/events.py +8 -1
  11. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/external_state_events.py +3 -3
  12. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/ramses_v2.py +3 -3
  13. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/uniswap_v3.py +4 -4
  14. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/hypercore.py +1 -1
  15. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/lending_health.py +1 -1
  16. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/substrates.py +19 -23
  17. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/types.py +25 -1
  18. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/validation.py +2 -2
  19. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/vault_data.py +16 -15
  20. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/vault_info.py +9 -4
  21. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_helpers.py +4 -4
  22. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_encoding.py +2 -2
  23. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_explorer.py +11 -2
  24. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_fuse_encoding.py +4 -4
  25. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_substrates.py +12 -12
  26. ipor_fusion-4.0.0.dev52/tests/test_types_and_events.py +122 -0
  27. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/typed_client/consumer.py +5 -0
  28. ipor_fusion-4.0.0.dev45/tests/test_types_and_events.py +0 -63
  29. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.claude-plugin/marketplace.json +0 -0
  30. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.claude-plugin/plugin.json +0 -0
  31. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.env.example +0 -0
  32. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.gemini/settings.json +0 -0
  33. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.git-blame-ignore-revs +0 -0
  34. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/cd.yml +0 -0
  35. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/ci.yml +0 -0
  36. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/commit-lint.yml +0 -0
  37. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/notify-slack.yml +0 -0
  38. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/python-build.yml +0 -0
  39. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/python-publish.yml +0 -0
  40. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/release.yml +0 -0
  41. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.github/workflows/report-slack.yml +0 -0
  42. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.gitignore +0 -0
  43. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.mcp.json +0 -0
  44. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.pre-commit-config.yaml +0 -0
  45. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/.python-version +0 -0
  46. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/AGENTS.md +0 -0
  47. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/CHANGELOG.md +0 -0
  48. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/CLAUDE.md +0 -0
  49. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/CODEOWNERS +0 -0
  50. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/LICENSE +0 -0
  51. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/build.sh +0 -0
  52. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/CONTRIBUTING.md +0 -0
  53. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/README.md +0 -0
  54. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  55. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  56. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  57. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  58. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/external_state_margin_leg_base.py +0 -0
  59. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/examples/simple_aave_v3_supply_base.py +0 -0
  60. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/pytest.ini +0 -0
  61. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/scripts/commit-check.sh +0 -0
  62. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/scripts/crosschain_readiness.py +0 -0
  63. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/skills/ipor-deploy-vault/SKILL.md +0 -0
  64. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/about.py +0 -0
  65. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cache.py +0 -0
  66. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/chains.py +0 -0
  67. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/__init__.py +0 -0
  68. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  69. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/config_cmd.py +0 -0
  70. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/config_store.py +0 -0
  71. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/main.py +0 -0
  72. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/market_cmd.py +0 -0
  73. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/options.py +0 -0
  74. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  75. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  76. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/vault_health.py +0 -0
  77. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  78. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  79. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/config/__init__.py +0 -0
  80. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/config/roles.py +0 -0
  81. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/__init__.py +0 -0
  82. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/access.py +0 -0
  83. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/address.py +0 -0
  84. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/contract.py +0 -0
  85. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/erc20.py +0 -0
  86. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/external_state_executor.py +0 -0
  87. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/fee_manager.py +0 -0
  88. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/fuse_action.py +0 -0
  89. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/fusion_factory.py +0 -0
  90. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/logs.py +0 -0
  91. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/multicall.py +0 -0
  92. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/oracle.py +0 -0
  93. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/plasma_vault.py +0 -0
  94. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/rewards_manager.py +0 -0
  95. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/simulation.py +0 -0
  96. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  97. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/__init__.py +0 -0
  98. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  99. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  100. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  101. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  102. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  103. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  104. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  105. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  106. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  107. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/contracts.py +0 -0
  108. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/discovery.py +0 -0
  109. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/errors.py +0 -0
  110. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/lane.py +0 -0
  111. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/logs.py +0 -0
  112. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/messages.py +0 -0
  113. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/simulation.py +0 -0
  114. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  115. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  116. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  117. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  118. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  119. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/crosschain/transport.py +0 -0
  120. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/entry_cli.py +0 -0
  121. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/entry_mcp.py +0 -0
  122. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/errors.py +0 -0
  123. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/field_docs.py +0 -0
  124. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/__init__.py +0 -0
  125. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  126. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/async_action.py +0 -0
  127. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/base.py +0 -0
  128. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  129. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  130. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  131. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  132. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  133. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/erc4626.py +0 -0
  134. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  135. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/external_state.py +0 -0
  136. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  137. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  138. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/hypercore.py +0 -0
  139. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  140. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/merkl.py +0 -0
  141. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/morpho.py +0 -0
  142. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/fuses/universal.py +0 -0
  143. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/guide/__init__.py +0 -0
  144. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/guide/architecture.md +0 -0
  145. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/guide/glossary.md +0 -0
  146. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/guide/invariants.md +0 -0
  147. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/guide/quickstart.md +0 -0
  148. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/market_ids.py +0 -0
  149. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/market_info.py +0 -0
  150. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/mcp/__init__.py +0 -0
  151. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/mcp/guide.py +0 -0
  152. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/mcp/models.py +0 -0
  153. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/mcp/server.py +0 -0
  154. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/morpho_api.py +0 -0
  155. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/py.typed +0 -0
  156. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/__init__.py +0 -0
  157. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/aave_v3.py +0 -0
  158. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/aave_v4.py +0 -0
  159. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/compound_v3.py +0 -0
  160. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  161. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/morpho.py +0 -0
  162. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  163. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/position_manager.py +0 -0
  164. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  165. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  166. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/src/ipor_fusion/readers/unpriceable.py +0 -0
  167. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/UniswapV3UniversalRouter.py +0 -0
  168. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_crosschain.py +0 -0
  169. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_crosschain_lifecycle.py +0 -0
  170. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_crosschain_recovery.py +0 -0
  171. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_euler_v2.py +0 -0
  172. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_foundry.py +0 -0
  173. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_guide.py +0 -0
  174. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_hypercore_model.py +0 -0
  175. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_hypercore_shadow.py +0 -0
  176. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_multicall.py +0 -0
  177. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_simulate.py +0 -0
  178. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/_validation.py +0 -0
  179. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/addresses.py +0 -0
  180. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/conftest.py +0 -0
  181. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/constants.py +0 -0
  182. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  183. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/hypercore_market55.json +0 -0
  184. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/vault_info_golden/clean.json +0 -0
  185. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  186. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/vault_info_golden/rich.json +0 -0
  187. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  188. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_about.py +0 -0
  189. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_access_manager.py +0 -0
  190. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cache.py +0 -0
  191. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_chains.py +0 -0
  192. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_changelog.py +0 -0
  193. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_commands.py +0 -0
  194. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_config_store.py +0 -0
  195. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_hypercore.py +0 -0
  196. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_lending_health.py +0 -0
  197. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_market_morpho_blue.py +0 -0
  198. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_oracle_mapping.py +0 -0
  199. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_pending_requests_scan.py +0 -0
  200. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_role_accounts.py +0 -0
  201. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_vault_info_golden.py +0 -0
  202. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_vault_predates_getters.py +0 -0
  203. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_cli_vault_validate.py +0 -0
  204. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_contract_call.py +0 -0
  205. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_contract_encoder.py +0 -0
  206. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_ccip_events.py +0 -0
  207. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_ccip_indexer.py +0 -0
  208. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_ccip_quote.py +0 -0
  209. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_chainlink.py +0 -0
  210. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_contracts.py +0 -0
  211. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_errors.py +0 -0
  212. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_lane.py +0 -0
  213. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_layering.py +0 -0
  214. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_readiness.py +0 -0
  215. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_readiness_report.py +0 -0
  216. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_crosschain_transport.py +0 -0
  217. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_erc20.py +0 -0
  218. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_examples_composed_hypercore.py +0 -0
  219. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_examples_crosschain.py +0 -0
  220. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  221. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_examples_vaults.py +0 -0
  222. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_external_state_events.py +0 -0
  223. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_external_state_executor.py +0 -0
  224. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_fee_manager.py +0 -0
  225. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_guide.py +0 -0
  226. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_deployment.py +0 -0
  227. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_events.py +0 -0
  228. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_flow.py +0 -0
  229. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_live.py +0 -0
  230. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_outcome.py +0 -0
  231. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_readers.py +0 -0
  232. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_hypercore_simulation.py +0 -0
  233. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_logs.py +0 -0
  234. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_mcp_hypercore.py +0 -0
  235. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_mcp_models.py +0 -0
  236. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_mcp_server.py +0 -0
  237. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_mcp_vault_validate.py +0 -0
  238. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_multicall.py +0 -0
  239. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_oracle.py +0 -0
  240. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_oracle_mapping.py +0 -0
  241. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_plasma_vault.py +0 -0
  242. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_reader_encoding.py +0 -0
  243. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_readiness_selection.py +0 -0
  244. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_revert_decoding.py +0 -0
  245. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_rewards_manager.py +0 -0
  246. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_roles.py +0 -0
  247. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_sdk_layering.py +0 -0
  248. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  249. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  250. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  251. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_crosschain_canary.py +0 -0
  252. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_crosschain_hypercore.py +0 -0
  253. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  254. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  255. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  256. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  257. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  258. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  259. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  260. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_examples_arbitrum.py +0 -0
  261. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_examples_base.py +0 -0
  262. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_examples_crosschain.py +0 -0
  263. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_external_state_executor_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_morpho_blue.py +0 -0
  268. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_pilot_v3.py +0 -0
  269. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  270. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  271. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  272. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_readers_integration.py +0 -0
  274. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_release_funds.py +0 -0
  275. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_swap_on_base.py +0 -0
  276. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  277. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_solidity_compiler.py +0 -0
  278. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_solidity_mirrors.py +0 -0
  279. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_unpriceable_tokens.py +0 -0
  280. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_validation.py +0 -0
  281. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_vault_dep_graph.py +0 -0
  282. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_vault_info_onchain_base.py +0 -0
  283. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  284. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_vault_simulator_blocks.py +0 -0
  285. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_web3_context.py +0 -0
  286. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/tests/test_withdraw_manager.py +0 -0
  287. {ipor_fusion-4.0.0.dev45 → ipor_fusion-4.0.0.dev52}/uv.lock +0 -0
@@ -0,0 +1,166 @@
1
+ # Migrating to ipor-fusion 4.0
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
+ All types are importable from `ipor_fusion` (and `ipor_fusion.types`);
18
+ `ChecksumAddress` and `HexAddress` come from `eth_typing`,
19
+ `BlockIdentifier` from `web3.types`, and `USDC_TOKEN_INDEX` / `hip3_action_asset` from
20
+ `ipor_fusion.fuses.hypercore`. The wrappers label values; they do not validate byte
21
+ 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 hex in any spelling → 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 hex in any spelling) |
35
+ | crosschain ids | `CommandId(b)`, `MessageId(b)`, `AssetId(b)`, `OperationId(b)`, `TransferId(b)`, `ReturnRequestId(b)`, `PositionSetHash(b)` (bytes32); `ProposalId(n)`, `DeploymentRequestId(n)`, `StargateAssetId(n)` (int) |
36
+ | 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)` |
37
+ | external state | `ProposalHash(b)` |
38
+ | `BlockIdentifier` | a block number, `"latest"` / `"pending"` / ..., or a block hash |
39
+
40
+ `MessageId` covers both a CCIP `messageId` and a LayerZero `guid`. `AssetId` is the
41
+ crosschain executor's `bytes32` asset id; a Stargate pool's `uint16` asset id is
42
+ `StargateAssetId`. `DeploymentRequestId` is a factory's `uint256` dispatcher request;
43
+ a `bytes32` return (recall) request is `ReturnRequestId`.
44
+
45
+ ## What changed, by area
46
+
47
+ **Vault reads and validation.** `fetch_vault_data(chain_id=)`, `fetch_deployment_info`,
48
+ `build_vault_info_json`, `ensure_supported_chain`, `fetch_vault_lending_health` take
49
+ `ChainId`; vault and oracle addresses
50
+ (`validate_vault`, `fetch_deployment_info`, `build_vault_info_json`,
51
+ `fetch_unpriceable_priced_tokens`) take `ChecksumAddress`; `resolve_token_symbol`,
52
+ `resolve_token_decimals`, `get_contract_name`, `get_deployment_tx` take `HexAddress`
53
+ (a checksum address fits). The `resolve_name` / `resolve_symbol` callbacks receive a
54
+ `HexAddress` (a callback typed for `str` still fits). Market-keyed data is keyed by
55
+ `MarketId`: `VaultData.market_substrates`, `dependency_graph`, `fuse_markets`,
56
+ `morpho_positions`, `aave_positions`; the `VaultState` protocol, `validate_vault_state`
57
+ (`market_balances`), `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens`
58
+ and the `dependency_graph` helpers (`compute_update_reach`, `compute_update_groups`,
59
+ `find_orphan_fuse_markets`, `find_markets_missing_erc20_dependency`).
60
+ A custom `VaultState` implementation also annotates `asset` as `ChecksumAddress` and
61
+ its `unpriceable_priced_tokens` items' `market_id` as `MarketId`
62
+ (`UnpriceableToken`), and `price_oracle_addr` / `fuse_markets` keys as
63
+ `ChecksumAddress`. `VaultData.asset`, `access_manager`, `price_oracle_addr`,
64
+ `rewards_manager`, `withdraw_manager`, `fuses` and `instant_fuses` (and the
65
+ `fuse_markets` keys), `FeeData.fee_manager` / `ipor_dao_fee_recipient`, and
66
+ `VaultValidation.vault` / `asset` are `ChecksumAddress`; `VaultData.balance_fuses`
67
+ is `list[BalanceFuse]` and `token_prices_usd` is keyed by `HexAddress` (lowercase);
68
+ the Uniswap V3 / Ramses V2 position events' `version`, `sender` and `recipient` are
69
+ `ChecksumAddress`;
70
+ `VaultValidation.chain_id` is `ChainId`; `LendingMarketHealth.market_id` and
71
+ `MiddlewarePricedToken.market_id` are `MarketId`; `Erc20Holding.address` is
72
+ `HexAddress`, and `SubstrateInfo.address` is `HexAddress | None` (`None`, no longer
73
+ `""`, when a substrate carries no address).
74
+ `LegacyPriceOracleMiddleware` takes `base_currency_decimals: Decimals`.
75
+ `get_logs_adaptive(chain_id=)` and `LogSource.get_logs` take `ChainId`;
76
+ `quote_ccip_native_fee(block=)` and `OracleMappingReader.asset_source_events` take a
77
+ `BlockIdentifier`.
78
+
79
+ **Markets and substrates.** Every `IporFusionMarkets` member is a `MarketId`.
80
+ `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
81
+
82
+ **Access.** `AccessManager.has_role`, `grant_role`, `revoke_role`,
83
+ `get_accounts_with_role` take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
84
+
85
+ **Morpho.** `MorphoBlueMarketId` now always carries `0x`; build it with
86
+ `parse_morpho_market_id` instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market`,
87
+ `build_morpho_blue_market_json` take it; `MorphoApiMarket.market_id`,
88
+ `VaultV1MarketAllocation.market_id`, `VaultV2Cap.market_id`,
89
+ `MorphoPositionBreakdown.market_id` (as read from substrates) and the keys of
90
+ `VaultV1Info.public_allocator_flow_caps` are canonical. `fetch_market`, `fetch_vault`,
91
+ `morpho_blue_address`, `build_*_json` take `ChainId`; `fetch_vault` takes a
92
+ `ChecksumAddress`. Every address on the `morpho_api` records is `ChecksumAddress`;
93
+ optional ones (`curator`, `guardian`, `fee_recipient`, `performance_fee_recipient`,
94
+ `management_fee_recipient`, `inner_vault`, `VaultFlowCap.admin`, the `VaultV2Cap`
95
+ market params) are `ChecksumAddress | None`.
96
+
97
+ **Crosschain.** The executor, dispatcher and factory wrappers, the lanes and
98
+ `StargateCrosschainCommandFuse.enter(request_id=)` take the crosschain ids above, and
99
+ their getters return them (`active_command`, `active_proposal_id`, `propose_balance`
100
+ on CCIP, `active_return` → `OperationId`, `asset_id`, `tracked_position_set_hash`,
101
+ `pending_request_by_chain`, `CrosschainLane.proposal_id_from_logs` /
102
+ `proposal_id_from_receipt`). Records carry them too: `Command`, `ActiveCommand`,
103
+ `ExecutorInitParams`, `BalanceObservation`, `LaneObservation`, `Observation`,
104
+ `CcipObservation`, `OutboundMessage`, `CcipMessageSent`, `Any2EVMMessage`,
105
+ `VerifierResult`, `Packet.guid`, `TaxiMessage`, `StargateChain`; and the helpers
106
+ `fetch_verifier_result(message_id=)`, `lz_receive_calldata(guid=)` and
107
+ `lz_compose_calldata(guid=)` take a `MessageId`. Decoded event values
108
+ (`ccip_events` → `CcipEvent.values`) stay a generic mapping: convert a value
109
+ before passing it on.
110
+
111
+ **HyperCore (preview).** The fuses, `HyperCoreSubstrates`, `HyperCoreReader`,
112
+ `read_hypercore_transfer_snapshot` and the helpers `system_address`, `read_index_of`,
113
+ `perp_dex_of`, `is_native_perp_asset`, `is_hip3_asset` take the HyperCore ids;
114
+ `HyperCorePerpAssetInfo`, `HyperCorePerpMarket`, `HyperCoreSpotLeg`,
115
+ `HyperCorePendingState` and `HyperCoreTransferSnapshot` carry them.
116
+
117
+ **External state.** `ExternalStateExecutor.proposal_hash` returns and `confirm_balance`
118
+ takes a `ProposalHash`; `BalanceProposal.proposal_hash` and `NavMark.proposal_hash`
119
+ are `ProposalHash`; `ExternalStateEvent.transaction_hash` is a `TxHash`.
120
+
121
+ **Errors and explorer.** `TransactionError(tx_hash=)` takes a `TxHash`,
122
+ `LogScanError(address=)` a `ChecksumAddress`; `get_deployment_tx` returns a `TxHash`
123
+ (a malformed hash from Etherscan is reported as the error `malformed-tx-hash`).
124
+
125
+ **MCP response models** (Python constructors only; the JSON schemas are unchanged
126
+ except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
127
+ `MarketId`. Calling an MCP tool function directly from Python takes `ChainId`.
128
+
129
+ **Moved, still importable.** `FuseAction` lives in `ipor_fusion.core.fuse_action` and
130
+ `ZERO_ADDRESS` in `ipor_fusion.types`; both are re-exported from
131
+ `ipor_fusion.fuses.base` and `FuseAction` from `ipor_fusion`. The CLI's internal
132
+ `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
133
+ `vault_markets_view` and `oracle_mapping_view` (private names, not API).
134
+
135
+ ## Behavior changes
136
+
137
+ - The MCP `vault_info` document reports the vault address checksummed (the CLI
138
+ already did).
139
+ - `SubstrateInfo.address` is `None` instead of `""` for a substrate without an
140
+ address. Truthiness checks are unaffected and the SDK's JSON documents already
141
+ omit it, but `dataclasses.asdict(info)` now gives `"address": None` and
142
+ `info.address == ""` is false.
143
+ - The Uniswap V3 / Ramses V2 position events (`extract_new_position_events`,
144
+ `extract_close_position_events`) return their addresses checksummed (they were the
145
+ lowercase ABI-decoded strings).
146
+ - `fusion market meta-morpho --json` and the MCP `market_meta_morpho` tool report
147
+ `null` instead of `""` for an absent curator, guardian or fee recipient; the MCP
148
+ schema's `curator` is nullable.
149
+ - The Morpho API client checksums every address it returns (market, vault,
150
+ allocation, adapter and cap records, and the `fusion market` / MCP market documents
151
+ built from them), so string comparisons against the API's own spelling change.
152
+ - It raises `MorphoApiError` for a market id or address it parses (market and V1
153
+ allocation ids, V1 flow-cap ids, vault/asset/owner/token/oracle/IRM/allocator
154
+ addresses) when the value is invalid, or absent where the field is optional in the
155
+ answer. A key the parser requires can instead raise `KeyError`, e.g. a `loanAsset`
156
+ without `address`. When matching a market's own allocation and flow cap, an id that
157
+ does not parse matches nothing, and a V1 flow cap without an id is skipped, as
158
+ before. `--market` rejects a malformed id.
159
+ - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
160
+ documents already were).
161
+ - `TransactionError.tx_hash` is `0x`-prefixed.
162
+ - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
163
+ a value that is not an address.
164
+ - The on-disk caches (`~/.cache/ipor-fusion/`) key addresses lowercased; older entries
165
+ are rekeyed on load, so a contract name cached for one spelling now also resolves
166
+ for the other.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev45
3
+ Version: 4.0.0.dev52
4
4
  Summary: The IPOR Fusion SDK for Python
5
5
  Project-URL: homepage, https://ipor.io
6
6
  Project-URL: repository, https://github.com/IPOR-Labs/ipor-fusion.py
@@ -115,6 +115,10 @@ alt="Release">
115
115
  </tr>
116
116
  </table>
117
117
 
118
+ > **Upgrading from 3.x?** 4.0 types the public API's ids and addresses and changes
119
+ > a few behaviors (Morpho market ids, nullable optional addresses); see
120
+ > [MIGRATION_4.md](MIGRATION_4.md).
121
+
118
122
  ## Quickstart
119
123
 
120
124
  ### Install
@@ -69,6 +69,10 @@ alt="Release">
69
69
  </tr>
70
70
  </table>
71
71
 
72
+ > **Upgrading from 3.x?** 4.0 types the public API's ids and addresses and changes
73
+ > a few behaviors (Morpho market ids, nullable optional addresses); see
74
+ > [MIGRATION_4.md](MIGRATION_4.md).
75
+
72
76
  ## Quickstart
73
77
 
74
78
  ### Install
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev45"
3
+ version = "4.0.0.dev52"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -165,3 +165,4 @@ packages = ["src/ipor_fusion"]
165
165
  # semantic-release before `build_command`, so each wheel carries its own entry.
166
166
  [tool.hatch.build.targets.wheel.force-include]
167
167
  "CHANGELOG.md" = "ipor_fusion/CHANGELOG.md"
168
+ "MIGRATION_4.md" = "ipor_fusion/MIGRATION_4.md"
@@ -345,7 +345,9 @@ from ipor_fusion.types import (
345
345
  TokenId,
346
346
  TransferId,
347
347
  TxHash,
348
+ parse_hex_address,
348
349
  parse_morpho_market_id,
350
+ parse_tx_hash,
349
351
  )
350
352
  from ipor_fusion.validation import (
351
353
  Erc20Holding,
@@ -624,7 +626,9 @@ __all__ = [
624
626
  "PayloadHash",
625
627
  "ProposalHash",
626
628
  "TxHash",
629
+ "parse_hex_address",
627
630
  "parse_morpho_market_id",
631
+ "parse_tx_hash",
628
632
  "AssetId",
629
633
  "CommandId",
630
634
  "DeploymentRequestId",
@@ -20,7 +20,7 @@ from ipor_fusion.substrates import (
20
20
  decode_substrate,
21
21
  format_market_label,
22
22
  )
23
- from ipor_fusion.types import ChainId, MarketId
23
+ from ipor_fusion.types import ChainId, MarketId, parse_hex_address
24
24
  from ipor_fusion.vault_data import (
25
25
  GET_MARKET_SUBSTRATES,
26
26
  VaultData,
@@ -213,7 +213,7 @@ def _format_token_amount(
213
213
  ctx: Web3Context,
214
214
  raw: int,
215
215
  token_address: HexAddress,
216
- prices_usd: dict[str, float] | None = None,
216
+ prices_usd: dict[HexAddress, float] | None = None,
217
217
  ) -> str:
218
218
  """Format a raw on-chain amount using cached ERC-20 symbol + decimals.
219
219
 
@@ -226,7 +226,9 @@ def _format_token_amount(
226
226
  return f"{raw} raw ({symbol})"
227
227
  base = f"{_format_amount(raw, decimals)} {symbol}"
228
228
  if prices_usd:
229
- usd_suffix = _format_usd(raw, decimals, prices_usd.get(token_address.lower()))
229
+ usd_suffix = _format_usd(
230
+ raw, decimals, prices_usd.get(parse_hex_address(token_address))
231
+ )
230
232
  return f"{base}{usd_suffix}"
231
233
  return base
232
234
 
@@ -259,7 +261,7 @@ def _print_dependency_graph(data: VaultData) -> None:
259
261
  def _print_fuse_section(
260
262
  title: str,
261
263
  fuses: Sequence[ChecksumAddress],
262
- fuse_markets: dict[str, MarketId] | None,
264
+ fuse_markets: dict[ChecksumAddress, MarketId] | None,
263
265
  market_substrates: dict[MarketId, list[bytes]] | None,
264
266
  chain_id: ChainId,
265
267
  api_key: str | None,
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  from eth_account import Account
6
- from eth_typing import ChecksumAddress, HexStr
6
+ from eth_typing import ChecksumAddress
7
7
  from hexbytes import HexBytes
8
8
  from requests import ConnectionError as RequestsConnectionError
9
9
  from requests import HTTPError, Timeout
@@ -25,7 +25,7 @@ from ipor_fusion.core.logs import (
25
25
  get_logs_adaptive,
26
26
  )
27
27
  from ipor_fusion.errors import LogSourceUnavailable, TransactionError, get_revert_reason
28
- from ipor_fusion.types import ChainId, TxHash
28
+ from ipor_fusion.types import ChainId, parse_tx_hash
29
29
 
30
30
  # web3 retries every allowlisted method on any HTTP error, a 400 included;
31
31
  # a too-wide eth_getLogs answered with HTTP 400/413 or a query timeout would be
@@ -229,7 +229,7 @@ class Web3Context:
229
229
  reason = get_revert_reason(self.web3, tx_hash, receipt)
230
230
  raise TransactionError(
231
231
  "Transaction failed",
232
- tx_hash=TxHash(HexStr(tx_hash.to_0x_hex())),
232
+ tx_hash=parse_tx_hash(bytes(tx_hash)),
233
233
  revert_reason=reason,
234
234
  )
235
235
  return receipt
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Mapping, Sequence, Set
6
6
 
7
+ from eth_typing import ChecksumAddress
8
+
7
9
  from ipor_fusion.market_ids import IporFusionMarkets
8
10
  from ipor_fusion.types import MarketId
9
11
 
@@ -38,7 +40,7 @@ _ERC20_DEP_NOT_REQUIRED: frozenset[MarketId] = frozenset(
38
40
 
39
41
 
40
42
  def find_orphan_fuse_markets(
41
- fuse_markets: Mapping[str, MarketId],
43
+ fuse_markets: Mapping[ChecksumAddress, MarketId],
42
44
  balance_fuse_market_ids: Set[MarketId],
43
45
  ) -> dict[MarketId, list[str]]:
44
46
  """Return markets claimed by action fuses but lacking a balance fuse.
@@ -10,10 +10,10 @@ import time
10
10
  from urllib.parse import urlencode
11
11
  from urllib.request import urlopen
12
12
 
13
- from eth_typing import HexAddress, HexStr
13
+ from eth_typing import HexAddress
14
14
 
15
15
  from ipor_fusion.cache import cache_key, load_contract_cache, update_contract_cache
16
- from ipor_fusion.types import ChainId, TxHash
16
+ from ipor_fusion.types import ChainId, TxHash, parse_tx_hash
17
17
 
18
18
  ETHERSCAN_V2_URL = "https://api.etherscan.io/v2/api"
19
19
 
@@ -67,6 +67,17 @@ def get_deployment_tx(
67
67
  return _fetch_contract_creation_tx(chain_id, address, api_key)
68
68
 
69
69
 
70
+ def _creation_tx_hash(raw: object) -> tuple[TxHash | None, str | None]:
71
+ if not raw:
72
+ return None, None
73
+ if not isinstance(raw, str):
74
+ return None, "malformed-tx-hash"
75
+ try:
76
+ return parse_tx_hash(raw), None
77
+ except ValueError:
78
+ return None, "malformed-tx-hash"
79
+
80
+
70
81
  def _fetch_contract_creation_tx(
71
82
  chain_id: ChainId, address: HexAddress, api_key: str | None = None
72
83
  ) -> tuple[TxHash | None, str | None]:
@@ -91,8 +102,7 @@ def _fetch_contract_creation_tx(
91
102
  with urlopen(url, timeout=10) as resp: # noqa: S310
92
103
  data = json.loads(resp.read().decode())
93
104
  if data.get("status") == "1" and data.get("result"):
94
- tx_hash = data["result"][0].get("txHash")
95
- return (TxHash(HexStr(tx_hash)) if tx_hash else None), None
105
+ return _creation_tx_hash(data["result"][0].get("txHash"))
96
106
  result_text = str(data.get("result", "")).lower()
97
107
  if "free api access is not supported" in result_text:
98
108
  return None, "etherscan-paid-tier-required"
@@ -14,5 +14,12 @@ def extract_events(
14
14
  for log in receipt["logs"]:
15
15
  if log["topics"][0] == sig_hash:
16
16
  decoded = decode(abi_types, log["data"])
17
- events.append(dataclass_type(*decoded))
17
+ events.append(
18
+ dataclass_type(
19
+ *(
20
+ Web3.to_checksum_address(value) if abi == "address" else value
21
+ for abi, value in zip(abi_types, decoded, strict=True)
22
+ )
23
+ )
24
+ )
18
25
  return events
@@ -28,11 +28,11 @@ from typing import Any
28
28
 
29
29
  from eth_abi import decode
30
30
  from eth_abi.exceptions import DecodingError
31
- from eth_typing import ChecksumAddress, HexStr
31
+ from eth_typing import ChecksumAddress
32
32
  from web3 import Web3
33
33
  from web3.types import TxReceipt
34
34
 
35
- from ipor_fusion.types import TxHash
35
+ from ipor_fusion.types import TxHash, parse_tx_hash
36
36
 
37
37
 
38
38
  @dataclass(frozen=True, slots=True)
@@ -196,7 +196,7 @@ def external_state_event_name(topic: bytes | str) -> str | None:
196
196
  def _hex(value: Any) -> TxHash | None:
197
197
  if value is None:
198
198
  return None
199
- return TxHash(HexStr("0x" + _as_bytes(value, "transactionHash").hex()))
199
+ return parse_tx_hash(_as_bytes(value, "transactionHash"))
200
200
 
201
201
 
202
202
  def decode_external_state_event(log: Mapping[str, Any]) -> ExternalStateEvent | None:
@@ -136,13 +136,13 @@ class RamsesClaimFuse(Fuse):
136
136
 
137
137
  @dataclass(slots=True)
138
138
  class RamsesNewPositionEvent:
139
- version: str
139
+ version: ChecksumAddress
140
140
  token_id: TokenId
141
141
  liquidity: Liquidity
142
142
  amount0: Amount
143
143
  amount1: Amount
144
- sender: str
145
- recipient: str
144
+ sender: ChecksumAddress
145
+ recipient: ChecksumAddress
146
146
  fee: Fee
147
147
  tick_lower: Tick
148
148
  tick_upper: Tick
@@ -140,13 +140,13 @@ class UniswapV3CollectFuse(Fuse):
140
140
 
141
141
  @dataclass(slots=True)
142
142
  class UniswapV3NewPositionEvent:
143
- version: str
143
+ version: ChecksumAddress
144
144
  token_id: TokenId
145
145
  liquidity: Liquidity
146
146
  amount0: Amount
147
147
  amount1: Amount
148
- sender: str
149
- recipient: str
148
+ sender: ChecksumAddress
149
+ recipient: ChecksumAddress
150
150
  fee: Fee
151
151
  tick_lower: Tick
152
152
  tick_upper: Tick
@@ -154,7 +154,7 @@ class UniswapV3NewPositionEvent:
154
154
 
155
155
  @dataclass(slots=True)
156
156
  class UniswapV3ClosePositionEvent:
157
- version: str
157
+ version: ChecksumAddress
158
158
  token_id: TokenId
159
159
 
160
160
 
@@ -499,7 +499,7 @@ def _market_layout(
499
499
  bitmap = 0
500
500
  for raw in substrates:
501
501
  info = decode_substrate(raw, HYPERCORE_MARKET)
502
- if info.type_label == "SPOT_TOKEN" and not info.is_error:
502
+ if info.type_label == "SPOT_TOKEN" and not info.is_error and info.address:
503
503
  index = HyperCoreTokenIndex(int(info.extra["token_index"]))
504
504
  if index in seen:
505
505
  continue
@@ -353,7 +353,7 @@ def _aave_v4_spokes(substrates: list[bytes]) -> list[ChecksumAddress]:
353
353
  spokes: dict[ChecksumAddress, None] = {}
354
354
  for sub in substrates:
355
355
  info = decode_substrate(sub, market_id=IporFusionMarkets.AAVE_V4)
356
- if info.type_label == "AAVE_V4_RESERVE":
356
+ if info.type_label == "AAVE_V4_RESERVE" and info.address:
357
357
  spokes[Web3.to_checksum_address(info.address)] = None
358
358
  return list(spokes)
359
359
 
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
  from collections.abc import Callable
16
16
  from dataclasses import dataclass, field
17
17
 
18
- from eth_typing import HexAddress, HexStr
18
+ from eth_typing import HexAddress
19
19
  from eth_utils import keccak
20
20
 
21
21
  from ipor_fusion.fuses.hypercore import (
@@ -25,12 +25,12 @@ from ipor_fusion.fuses.hypercore import (
25
25
  read_index_of,
26
26
  )
27
27
  from ipor_fusion.market_ids import IporFusionMarkets
28
- from ipor_fusion.types import HyperCoreAsset, MarketId
28
+ from ipor_fusion.types import HyperCoreAsset, MarketId, parse_hex_address
29
29
 
30
30
 
31
31
  @dataclass
32
32
  class SubstrateInfo:
33
- address: HexAddress = HexAddress(HexStr(""))
33
+ address: HexAddress | None = None
34
34
  raw_hex: str = ""
35
35
  type_label: str = ""
36
36
  is_error: bool = False
@@ -47,14 +47,10 @@ class SubstrateInfo:
47
47
  # Slippage variant: [2 type chars][62 value chars]
48
48
 
49
49
 
50
- def _hex_address(hex_digits: str) -> HexAddress:
51
- return HexAddress(HexStr(f"0x{hex_digits}"))
52
-
53
-
54
50
  def _decode_type_lshift160(hex_str: str, types: dict[int, str]) -> SubstrateInfo:
55
51
  """Decode type<<160 | address (Ebisu, Midas, Balancer, Velodrome)."""
56
52
  type_byte = int(hex_str[22:24], 16)
57
- addr = _hex_address(hex_str[24:])
53
+ addr = parse_hex_address(hex_str[24:])
58
54
  label = types.get(type_byte, f"type={type_byte}")
59
55
  return SubstrateInfo(address=addr, type_label=label)
60
56
 
@@ -67,13 +63,13 @@ def _decode_type_lshift248(hex_str: str, types: dict[int, str]) -> SubstrateInfo
67
63
  return SubstrateInfo(
68
64
  raw_hex=f"0x{hex_str}", type_label=label, extra={"slippage": str(value)}
69
65
  )
70
- addr = _hex_address(hex_str[24:])
66
+ addr = parse_hex_address(hex_str[24:])
71
67
  return SubstrateInfo(address=addr, type_label=label)
72
68
 
73
69
 
74
70
  def _decode_plain_address(hex_str: str) -> SubstrateInfo:
75
71
  """Decode zero-padded address: 12 zero bytes + 20 address bytes."""
76
- return SubstrateInfo(address=_hex_address(hex_str[24:]))
72
+ return SubstrateInfo(address=parse_hex_address(hex_str[24:]))
77
73
 
78
74
 
79
75
  def _decode_morpho(hex_str: str) -> SubstrateInfo:
@@ -88,14 +84,14 @@ def _decode_uniswap_v4(hex_str: str) -> SubstrateInfo:
88
84
  substrate-as-asset (12 zero bytes + address)."""
89
85
  if hex_str[:24] == "0" * 24:
90
86
  return SubstrateInfo(
91
- address=_hex_address(hex_str[24:]), type_label="uniswap_v4_token"
87
+ address=parse_hex_address(hex_str[24:]), type_label="uniswap_v4_token"
92
88
  )
93
89
  return SubstrateInfo(raw_hex=f"0x{hex_str}", type_label="uniswap_v4_pool_id")
94
90
 
95
91
 
96
92
  def _decode_enso(hex_str: str) -> SubstrateInfo:
97
93
  """Decode address<<96 | selector<<64 (Enso)."""
98
- addr = _hex_address(hex_str[0:40])
94
+ addr = parse_hex_address(hex_str[0:40])
99
95
  selector = f"0x{hex_str[40:48]}"
100
96
  return SubstrateInfo(
101
97
  address=addr, type_label="ENSO_TARGET", extra={"selector": selector}
@@ -104,7 +100,7 @@ def _decode_enso(hex_str: str) -> SubstrateInfo:
104
100
 
105
101
  def _decode_dolomite(hex_str: str) -> SubstrateInfo:
106
102
  """Decode asset<<96 | subAccountId<<88 | canBorrow<<80 (Dolomite)."""
107
- addr = _hex_address(hex_str[0:40])
103
+ addr = parse_hex_address(hex_str[0:40])
108
104
  sub_account_id = int(hex_str[40:42], 16)
109
105
  can_borrow = (int(hex_str[42:44], 16) & 0x01) == 1
110
106
  return SubstrateInfo(
@@ -131,7 +127,7 @@ def _decode_aave_v4(hex_str: str) -> SubstrateInfo:
131
127
  return SubstrateInfo(raw_hex=f"0x{hex_str}", type_label=label)
132
128
  flags = int(hex_str[50:52], 16)
133
129
  return SubstrateInfo(
134
- address=_hex_address(hex_str[2:42]),
130
+ address=parse_hex_address(hex_str[2:42]),
135
131
  type_label="AAVE_V4_RESERVE",
136
132
  extra={
137
133
  "reserve_id": str(int(hex_str[42:50], 16)),
@@ -149,7 +145,7 @@ def _decode_euler_v2(hex_str: str) -> SubstrateInfo:
149
145
  plain-address path silently produces a malformed address (last 20 bytes are
150
146
  flag bytes + zero padding).
151
147
  """
152
- addr = _hex_address(hex_str[0:40])
148
+ addr = parse_hex_address(hex_str[0:40])
153
149
  is_collateral = (int(hex_str[40:42], 16) & 0x01) == 1
154
150
  can_borrow = (int(hex_str[42:44], 16) & 0x01) == 1
155
151
  sub_account = f"0x{hex_str[44:46]}"
@@ -178,13 +174,13 @@ def _decode_async_action(hex_str: str) -> SubstrateInfo:
178
174
  type_byte = int(hex_str[0:2], 16)
179
175
  if type_byte == 0:
180
176
  return SubstrateInfo(
181
- address=_hex_address(hex_str[2:42]),
177
+ address=parse_hex_address(hex_str[2:42]),
182
178
  type_label="ALLOWED_AMOUNT_TO_OUTSIDE",
183
179
  extra={"amount": str(int(hex_str[42:64], 16))},
184
180
  )
185
181
  if type_byte == 1:
186
182
  return SubstrateInfo(
187
- address=_hex_address(hex_str[16:56]),
183
+ address=parse_hex_address(hex_str[16:56]),
188
184
  type_label="ALLOWED_TARGETS",
189
185
  extra={"selector": f"0x{hex_str[56:64]}"},
190
186
  )
@@ -229,10 +225,10 @@ def _decode_external_state(hex_str: str) -> SubstrateInfo:
229
225
  if type_byte == 0:
230
226
  return SubstrateInfo(raw_hex=f"0x{hex_str}", type_label="UNDEFINED")
231
227
  if label := _EXTERNAL_STATE_ADDRESS_TYPES.get(type_byte):
232
- return SubstrateInfo(address=_hex_address(hex_str[24:]), type_label=label)
228
+ return SubstrateInfo(address=parse_hex_address(hex_str[24:]), type_label=label)
233
229
  if type_byte == 2:
234
230
  return SubstrateInfo(
235
- address=_hex_address(hex_str[24:]),
231
+ address=parse_hex_address(hex_str[24:]),
236
232
  type_label="TARGET",
237
233
  extra={"selector": f"0x{hex_str[16:24]}"},
238
234
  )
@@ -270,7 +266,7 @@ def _decode_crosschain(hex_str: str) -> SubstrateInfo:
270
266
  is_error=True,
271
267
  extra={"chain_id": str(chain_id)},
272
268
  )
273
- info = SubstrateInfo(address=_hex_address(hex_str[24:]), type_label=label)
269
+ info = SubstrateInfo(address=parse_hex_address(hex_str[24:]), type_label=label)
274
270
  if type_byte == 2:
275
271
  info.extra = {"chain_id": str(chain_id)}
276
272
  return info
@@ -330,7 +326,7 @@ def _decode_hypercore(hex_str: str) -> SubstrateInfo:
330
326
  return _hypercore_invalid(hex_str, label)
331
327
  if type_byte == 1:
332
328
  return SubstrateInfo(
333
- address=_hex_address(hex_str[24:]),
329
+ address=parse_hex_address(hex_str[24:]),
334
330
  type_label=label,
335
331
  extra={"token_index": str(int(hex_str[8:24], 16))},
336
332
  )
@@ -351,10 +347,10 @@ def _decode_hypercore(hex_str: str) -> SubstrateInfo:
351
347
  extra["read_index"] = str(read_index_of(asset))
352
348
  return SubstrateInfo(raw_hex=f"0x{hex_str}", type_label=label, extra=extra)
353
349
  if type_byte == 3:
354
- return SubstrateInfo(address=_hex_address(hex_str[24:]), type_label=label)
350
+ return SubstrateInfo(address=parse_hex_address(hex_str[24:]), type_label=label)
355
351
  if type_byte == 4:
356
352
  return SubstrateInfo(
357
- address=_hex_address(hex_str[24:]),
353
+ address=parse_hex_address(hex_str[24:]),
358
354
  type_label=label,
359
355
  extra={"max_fee_rate_decibps": str(int(hex_str[8:24], 16))},
360
356
  )
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import ClassVar, Literal, NewType, TypeAlias
3
3
 
4
- from eth_typing import ChecksumAddress, HexStr
4
+ from eth_typing import ChecksumAddress, HexAddress, HexStr
5
5
 
6
6
  Amount = NewType("Amount", int)
7
7
  Shares = NewType("Shares", int)
@@ -131,3 +131,27 @@ def parse_morpho_market_id(value: str | bytes) -> MorphoBlueMarketId:
131
131
  if len(digits) != 64 or any(c not in "0123456789abcdef" for c in digits):
132
132
  raise ValueError(f"invalid Morpho market ID (expected 32-byte hex): {value}")
133
133
  return MorphoBlueMarketId("0x" + digits)
134
+
135
+
136
+ def _hex_digits(value: str | bytes, size: int, what: str) -> str:
137
+ if isinstance(value, bytes):
138
+ if len(value) != size:
139
+ raise ValueError(f"invalid {what} (expected {size} bytes): {value!r}")
140
+ return value.hex()
141
+ digits = value.strip().removeprefix("0x").removeprefix("0X").lower()
142
+ if len(digits) != 2 * size or any(c not in "0123456789abcdef" for c in digits):
143
+ raise ValueError(f"invalid {what} (expected {size}-byte hex): {value}")
144
+ return digits
145
+
146
+
147
+ def parse_hex_address(value: str | bytes) -> HexAddress:
148
+ """An address as ``0x`` and 40 lowercase hex digits, from 20 raw bytes or hex
149
+ in any spelling; raises ``ValueError`` on anything else. For a checksummed
150
+ address use ``Web3.to_checksum_address``."""
151
+ return HexAddress(HexStr("0x" + _hex_digits(value, 20, "address")))
152
+
153
+
154
+ def parse_tx_hash(value: str | bytes) -> TxHash:
155
+ """A transaction hash as ``0x`` and 64 lowercase hex digits, from 32 raw bytes
156
+ or hex in any spelling; raises ``ValueError`` on anything else."""
157
+ return TxHash(HexStr("0x" + _hex_digits(value, 32, "transaction hash")))