ipor-fusion 4.0.0.dev35__tar.gz → 4.0.0.dev39__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 (285) hide show
  1. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/PKG-INFO +1 -1
  2. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/pyproject.toml +1 -1
  3. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/config_store.py +13 -2
  4. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/market_cmd.py +7 -7
  5. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/vault_cmd.py +1 -1
  6. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/vault_markets_view.py +1 -1
  7. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/market_info.py +1 -1
  8. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/mcp/models.py +1 -1
  9. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/mcp/server.py +5 -1
  10. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/morpho_api.py +89 -63
  11. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/validation.py +3 -3
  12. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/vault_data.py +1 -1
  13. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/vault_info.py +1 -1
  14. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_helpers.py +31 -2
  15. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_market_morpho_blue.py +55 -0
  16. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_mcp_models.py +19 -0
  17. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_mcp_server.py +14 -2
  18. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.claude-plugin/marketplace.json +0 -0
  19. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.claude-plugin/plugin.json +0 -0
  20. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.env.example +0 -0
  21. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.gemini/settings.json +0 -0
  22. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.git-blame-ignore-revs +0 -0
  23. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/cd.yml +0 -0
  24. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/ci.yml +0 -0
  25. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/commit-lint.yml +0 -0
  26. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/notify-slack.yml +0 -0
  27. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/python-build.yml +0 -0
  28. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/python-publish.yml +0 -0
  29. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/release.yml +0 -0
  30. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.github/workflows/report-slack.yml +0 -0
  31. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.gitignore +0 -0
  32. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.mcp.json +0 -0
  33. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.pre-commit-config.yaml +0 -0
  34. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/.python-version +0 -0
  35. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/AGENTS.md +0 -0
  36. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/CHANGELOG.md +0 -0
  37. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/CLAUDE.md +0 -0
  38. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/CODEOWNERS +0 -0
  39. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/LICENSE +0 -0
  40. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/README.md +0 -0
  41. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/build.sh +0 -0
  42. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/CONTRIBUTING.md +0 -0
  43. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/README.md +0 -0
  44. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  45. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  46. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  47. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  48. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/external_state_margin_leg_base.py +0 -0
  49. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/examples/simple_aave_v3_supply_base.py +0 -0
  50. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/pytest.ini +0 -0
  51. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/scripts/commit-check.sh +0 -0
  52. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/scripts/crosschain_readiness.py +0 -0
  53. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/skills/ipor-deploy-vault/SKILL.md +0 -0
  54. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/__init__.py +0 -0
  55. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/about.py +0 -0
  56. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cache.py +0 -0
  57. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/chains.py +0 -0
  58. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/__init__.py +0 -0
  59. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  60. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/config_cmd.py +0 -0
  61. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/main.py +0 -0
  62. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/options.py +0 -0
  63. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  64. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/vault_health.py +0 -0
  65. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  66. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  67. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/config/__init__.py +0 -0
  68. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/config/roles.py +0 -0
  69. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/__init__.py +0 -0
  70. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/access.py +0 -0
  71. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/address.py +0 -0
  72. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/context.py +0 -0
  73. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/contract.py +0 -0
  74. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/erc20.py +0 -0
  75. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/external_state_executor.py +0 -0
  76. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/fee_manager.py +0 -0
  77. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/fuse_action.py +0 -0
  78. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/fusion_factory.py +0 -0
  79. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/logs.py +0 -0
  80. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/multicall.py +0 -0
  81. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/oracle.py +0 -0
  82. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/plasma_vault.py +0 -0
  83. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/rewards_manager.py +0 -0
  84. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/simulation.py +0 -0
  85. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  86. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/__init__.py +0 -0
  87. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  88. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  89. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  90. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  91. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  92. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  93. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  94. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  95. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  96. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/contracts.py +0 -0
  97. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/discovery.py +0 -0
  98. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/errors.py +0 -0
  99. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/lane.py +0 -0
  100. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/logs.py +0 -0
  101. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/messages.py +0 -0
  102. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/simulation.py +0 -0
  103. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  104. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  105. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  106. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  107. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  108. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/crosschain/transport.py +0 -0
  109. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/dependency_graph.py +0 -0
  110. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/entry_cli.py +0 -0
  111. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/entry_mcp.py +0 -0
  112. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/errors.py +0 -0
  113. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/explorer.py +0 -0
  114. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/field_docs.py +0 -0
  115. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/__init__.py +0 -0
  116. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  117. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/async_action.py +0 -0
  118. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/base.py +0 -0
  119. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  120. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  121. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  122. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  123. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  124. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/erc4626.py +0 -0
  125. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  126. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/events.py +0 -0
  127. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/external_state.py +0 -0
  128. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  129. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  130. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  131. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/hypercore.py +0 -0
  132. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  133. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/merkl.py +0 -0
  134. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/morpho.py +0 -0
  135. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  136. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  137. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/fuses/universal.py +0 -0
  138. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/guide/__init__.py +0 -0
  139. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/guide/architecture.md +0 -0
  140. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/guide/glossary.md +0 -0
  141. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/guide/invariants.md +0 -0
  142. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/guide/quickstart.md +0 -0
  143. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/market_ids.py +0 -0
  144. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/mcp/__init__.py +0 -0
  145. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/mcp/guide.py +0 -0
  146. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/py.typed +0 -0
  147. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/__init__.py +0 -0
  148. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/aave_v3.py +0 -0
  149. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/aave_v4.py +0 -0
  150. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/compound_v3.py +0 -0
  151. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/hypercore.py +0 -0
  152. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  153. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/lending_health.py +0 -0
  154. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/morpho.py +0 -0
  155. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  156. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/position_manager.py +0 -0
  157. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  158. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  159. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/readers/unpriceable.py +0 -0
  160. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/substrates.py +0 -0
  161. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/src/ipor_fusion/types.py +0 -0
  162. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/UniswapV3UniversalRouter.py +0 -0
  163. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_crosschain.py +0 -0
  164. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_crosschain_lifecycle.py +0 -0
  165. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_crosschain_recovery.py +0 -0
  166. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_euler_v2.py +0 -0
  167. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_foundry.py +0 -0
  168. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_guide.py +0 -0
  169. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_hypercore_model.py +0 -0
  170. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_hypercore_shadow.py +0 -0
  171. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_multicall.py +0 -0
  172. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_simulate.py +0 -0
  173. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/_validation.py +0 -0
  174. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/addresses.py +0 -0
  175. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/conftest.py +0 -0
  176. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/constants.py +0 -0
  177. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  178. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/hypercore_market55.json +0 -0
  179. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/vault_info_golden/clean.json +0 -0
  180. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  181. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/vault_info_golden/rich.json +0 -0
  182. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  183. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_about.py +0 -0
  184. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_access_manager.py +0 -0
  185. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cache.py +0 -0
  186. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_chains.py +0 -0
  187. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_changelog.py +0 -0
  188. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_commands.py +0 -0
  189. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_config_store.py +0 -0
  190. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_hypercore.py +0 -0
  191. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_lending_health.py +0 -0
  192. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_oracle_mapping.py +0 -0
  193. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_pending_requests_scan.py +0 -0
  194. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_role_accounts.py +0 -0
  195. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_vault_info_golden.py +0 -0
  196. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_vault_predates_getters.py +0 -0
  197. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_cli_vault_validate.py +0 -0
  198. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_contract_call.py +0 -0
  199. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_contract_encoder.py +0 -0
  200. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_ccip_events.py +0 -0
  201. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_ccip_indexer.py +0 -0
  202. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_ccip_quote.py +0 -0
  203. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_chainlink.py +0 -0
  204. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_contracts.py +0 -0
  205. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_encoding.py +0 -0
  206. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_errors.py +0 -0
  207. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_lane.py +0 -0
  208. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_layering.py +0 -0
  209. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_readiness.py +0 -0
  210. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_readiness_report.py +0 -0
  211. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_crosschain_transport.py +0 -0
  212. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_erc20.py +0 -0
  213. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_examples_composed_hypercore.py +0 -0
  214. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_examples_crosschain.py +0 -0
  215. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  216. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_examples_vaults.py +0 -0
  217. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_explorer.py +0 -0
  218. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_external_state_events.py +0 -0
  219. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_external_state_executor.py +0 -0
  220. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_fee_manager.py +0 -0
  221. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_fuse_encoding.py +0 -0
  222. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_guide.py +0 -0
  223. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_deployment.py +0 -0
  224. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_events.py +0 -0
  225. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_flow.py +0 -0
  226. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_live.py +0 -0
  227. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_outcome.py +0 -0
  228. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_readers.py +0 -0
  229. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_hypercore_simulation.py +0 -0
  230. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_logs.py +0 -0
  231. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_mcp_hypercore.py +0 -0
  232. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_mcp_vault_validate.py +0 -0
  233. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_multicall.py +0 -0
  234. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_oracle.py +0 -0
  235. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_oracle_mapping.py +0 -0
  236. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_plasma_vault.py +0 -0
  237. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_reader_encoding.py +0 -0
  238. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_readiness_selection.py +0 -0
  239. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_revert_decoding.py +0 -0
  240. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_rewards_manager.py +0 -0
  241. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_roles.py +0 -0
  242. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_sdk_layering.py +0 -0
  243. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  244. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  245. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  246. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_crosschain_canary.py +0 -0
  247. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_crosschain_hypercore.py +0 -0
  248. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  249. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  250. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  251. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  252. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  253. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  254. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  255. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_examples_arbitrum.py +0 -0
  256. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_examples_base.py +0 -0
  257. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_examples_crosschain.py +0 -0
  258. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_external_state_executor_base.py +0 -0
  259. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  260. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  261. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  262. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_morpho_blue.py +0 -0
  263. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_pilot_v3.py +0 -0
  264. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  265. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  266. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  267. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  268. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_readers_integration.py +0 -0
  269. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_release_funds.py +0 -0
  270. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_swap_on_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_solidity_compiler.py +0 -0
  273. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_solidity_mirrors.py +0 -0
  274. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_substrates.py +0 -0
  275. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_types_and_events.py +0 -0
  276. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_unpriceable_tokens.py +0 -0
  277. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_validation.py +0 -0
  278. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_vault_dep_graph.py +0 -0
  279. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_vault_info_onchain_base.py +0 -0
  280. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  281. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_vault_simulator_blocks.py +0 -0
  282. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_web3_context.py +0 -0
  283. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/test_withdraw_manager.py +0 -0
  284. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/tests/typed_client/consumer.py +0 -0
  285. {ipor_fusion-4.0.0.dev35 → ipor_fusion-4.0.0.dev39}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev35
3
+ Version: 4.0.0.dev39
4
4
  Summary: The IPOR Fusion SDK for Python
5
5
  Project-URL: homepage, https://ipor.io
6
6
  Project-URL: repository, https://github.com/IPOR-Labs/ipor-fusion.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev35"
3
+ version = "4.0.0.dev39"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -6,7 +6,9 @@ from pathlib import Path
6
6
  from urllib.parse import urlsplit
7
7
 
8
8
  import click
9
- from pydantic import BaseModel, Field, ValidationError, model_validator
9
+ from eth_typing import ChecksumAddress
10
+ from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
11
+ from web3 import Web3
10
12
 
11
13
  from ipor_fusion.types import ChainId
12
14
 
@@ -22,10 +24,19 @@ CONFIG_VERSION = 1
22
24
 
23
25
 
24
26
  class VaultEntry(BaseModel):
25
- address: str
27
+ address: ChecksumAddress
26
28
  label: str
27
29
  chain_id: ChainId
28
30
 
31
+ @field_validator("address", mode="before")
32
+ @classmethod
33
+ def _checksum(cls, value: object) -> object:
34
+ # Configs written before 4.0 may hold any spelling; a value that is not
35
+ # an address is left for the commands to report, not for every load to fail.
36
+ if isinstance(value, str) and Web3.is_address(value):
37
+ return Web3.to_checksum_address(value)
38
+ return value
39
+
29
40
 
30
41
  class FusionConfig(BaseModel):
31
42
  providers: dict[str, str] = Field(default_factory=dict)
@@ -389,12 +389,12 @@ def _print_vault_v2(
389
389
  click.echo("")
390
390
  click.echo("Roles:")
391
391
  click.echo(f" Owner: {vault.owner}")
392
- click.echo(f" Curator: {vault.curator}")
392
+ click.echo(f" Curator: {vault.curator or ''}")
393
393
  click.echo(
394
- f" Performance fee: {vault.performance_fee * 100:.2f}% → {vault.performance_fee_recipient}"
394
+ f" Performance fee: {vault.performance_fee * 100:.2f}% → {vault.performance_fee_recipient or ''}"
395
395
  )
396
396
  click.echo(
397
- f" Management fee: {vault.management_fee * 100:.2f}% → {vault.management_fee_recipient}"
397
+ f" Management fee: {vault.management_fee * 100:.2f}% → {vault.management_fee_recipient or ''}"
398
398
  )
399
399
  click.echo(f" Allocators ({len(vault.allocators)}):")
400
400
  for addr in vault.allocators or ["(none)"]:
@@ -515,13 +515,13 @@ def _print_vault_v1(
515
515
  f"({_format_amount(vault.total_assets, vault.asset_decimals)} total)"
516
516
  )
517
517
  click.echo(
518
- f" Performance fee: {vault.fee_wad / WAD * 100:.2f}% → {vault.fee_recipient}"
518
+ f" Performance fee: {vault.fee_wad / WAD * 100:.2f}% → {vault.fee_recipient or ''}"
519
519
  )
520
520
  click.echo("")
521
521
  click.echo("Roles:")
522
522
  click.echo(f" Owner: {vault.owner}")
523
- click.echo(f" Curator: {vault.curator}")
524
- click.echo(f" Guardian: {vault.guardian}")
523
+ click.echo(f" Curator: {vault.curator or ''}")
524
+ click.echo(f" Guardian: {vault.guardian or ''}")
525
525
  click.echo(f" Allocators ({len(vault.allocators)}):")
526
526
  for addr in vault.allocators or ["(none)"]:
527
527
  tag = (
@@ -551,7 +551,7 @@ def _print_vault_v1(
551
551
  return
552
552
  rows: list[tuple[str, ...]] = []
553
553
  for alloc in allocations:
554
- flow = vault.public_allocator_flow_caps.get(alloc.market_id.lower(), (0, 0))
554
+ flow = vault.public_allocator_flow_caps.get(alloc.market_id, (0, 0))
555
555
  flow_str = (
556
556
  f"in {_format_amount(flow[0], alloc.loan_decimals)}" if flow[0] else "—"
557
557
  )
@@ -95,7 +95,7 @@ def vault() -> None:
95
95
  default=None,
96
96
  help="Chain ID or name (auto-detected when only one provider is configured).",
97
97
  )
98
- def add(address: str, label: str | None, chain_id: ChainId | None) -> None:
98
+ def add(address: ChecksumAddress, label: str | None, chain_id: ChainId | None) -> None:
99
99
  """Save a vault to the config."""
100
100
  cfg = load_config()
101
101
 
@@ -53,7 +53,7 @@ def _print_lending_health( # noqa: C901
53
53
  for pb in positions:
54
54
  coll_sym = resolve_token_symbol(ctx, pb.collateral_token) or "?"
55
55
  loan_sym = resolve_token_symbol(ctx, pb.loan_token) or "?"
56
- click.echo(f" morpho market 0x{pb.market_id} ({coll_sym}/{loan_sym}):")
56
+ click.echo(f" morpho market {pb.market_id} ({coll_sym}/{loan_sym}):")
57
57
  click.echo(
58
58
  f" Collateral: {_format_token_amount(ctx, int(pb.collateral), pb.collateral_token, prices)}"
59
59
  )
@@ -34,7 +34,7 @@ def build_morpho_blue_market_json(
34
34
  market's parameters, state and rates (from `MorphoReader`), plus the
35
35
  MetaMorpho vaults supplying it when ``api_market`` (`fetch_market`) is
36
36
  given, or ``api_error`` when that lookup failed. ``market_id`` is the
37
- 64-hex-char id without ``0x``."""
37
+ canonical id (`parse_morpho_market_id`) and is written as is."""
38
38
  out: dict = {
39
39
  "market_id": market_id,
40
40
  "chain_id": chain_id,
@@ -944,7 +944,7 @@ class MetaMorphoVaultResponse(_Base):
944
944
  allocations: list[dict[str, Any]] | None = None
945
945
  # shared
946
946
  owner: str
947
- curator: str
947
+ curator: str | None = None
948
948
  allocators: list[str]
949
949
 
950
950
 
@@ -464,7 +464,11 @@ def vault_add(
464
464
  save_config(cfg)
465
465
  return ActionResult(message=f"Vault {label} ({address}) updated.")
466
466
 
467
- cfg.vaults.append(VaultEntry(address=address, label=label, chain_id=chain_id))
467
+ cfg.vaults.append(
468
+ VaultEntry(
469
+ address=Web3.to_checksum_address(address), label=label, chain_id=chain_id
470
+ )
471
+ )
468
472
  save_config(cfg)
469
473
  return ActionResult(message=f"Vault {label} ({address}) added.")
470
474
 
@@ -14,6 +14,7 @@ from urllib.error import URLError
14
14
  from urllib.request import Request, urlopen
15
15
 
16
16
  from eth_abi import decode, encode
17
+ from eth_typing import ChecksumAddress
17
18
  from eth_utils import keccak
18
19
  from web3 import Web3
19
20
 
@@ -97,7 +98,7 @@ class VaultFlowCap:
97
98
  class VaultAllocation:
98
99
  """A MetaMorpho vault's exposure to a single Morpho Blue market."""
99
100
 
100
- vault_address: str
101
+ vault_address: ChecksumAddress
101
102
  vault_name: str
102
103
  vault_symbol: str
103
104
  asset_symbol: str
@@ -115,10 +116,10 @@ class MorphoApiMarket:
115
116
 
116
117
  market_id: MorphoBlueMarketId
117
118
  lltv: int
118
- loan_token: str
119
+ loan_token: ChecksumAddress
119
120
  loan_symbol: str
120
121
  loan_decimals: int
121
- collateral_token: str
122
+ collateral_token: ChecksumAddress
122
123
  collateral_symbol: str
123
124
  collateral_decimals: int
124
125
  oracle: str
@@ -179,13 +180,14 @@ def fetch_market(
179
180
 
180
181
  def _parse_market(raw: dict) -> MorphoApiMarket:
181
182
  state = raw.get("state") or {}
183
+ market_id = _api_market_id(raw.get("marketId"))
182
184
  return MorphoApiMarket(
183
- market_id=_api_market_id(raw.get("marketId")),
185
+ market_id=market_id,
184
186
  lltv=int(raw["lltv"]),
185
- loan_token=raw["loanAsset"]["address"],
187
+ loan_token=_api_address(raw["loanAsset"]["address"]),
186
188
  loan_symbol=raw["loanAsset"]["symbol"],
187
189
  loan_decimals=int(raw["loanAsset"]["decimals"]),
188
- collateral_token=raw["collateralAsset"]["address"],
190
+ collateral_token=_api_address(raw["collateralAsset"]["address"]),
189
191
  collateral_symbol=raw["collateralAsset"]["symbol"],
190
192
  collateral_decimals=int(raw["collateralAsset"]["decimals"]),
191
193
  oracle=raw["oracle"]["address"],
@@ -198,9 +200,7 @@ def _parse_market(raw: dict) -> MorphoApiMarket:
198
200
  borrow_apy=float(state.get("borrowApy") or 0.0),
199
201
  fee_wad=int(state.get("fee") or 0),
200
202
  timestamp=int(state.get("timestamp") or 0),
201
- vaults=[
202
- _parse_vault(v, raw["marketId"]) for v in raw.get("supplyingVaults") or []
203
- ],
203
+ vaults=[_parse_vault(v, market_id) for v in raw.get("supplyingVaults") or []],
204
204
  )
205
205
 
206
206
 
@@ -281,8 +281,8 @@ class VaultV2Cap:
281
281
  relative_cap_wad: int
282
282
  allocation: int
283
283
  market_id: MorphoBlueMarketId | None
284
- loan_token: str | None
285
- collateral_token: str | None
284
+ loan_token: ChecksumAddress | None
285
+ collateral_token: ChecksumAddress | None
286
286
  oracle: str | None
287
287
  irm: str | None
288
288
  lltv: int | None
@@ -297,20 +297,22 @@ class VaultV2Cap:
297
297
  class VaultV2Adapter:
298
298
  """An adapter contract attached to a Vault V2 (e.g. Morpho adapter)."""
299
299
 
300
- address: str
300
+ address: ChecksumAddress
301
301
  adapter_type: str
302
302
  assets: int
303
- inner_vault: str | None # for MorphoVaultV2Adapter: the underlying market vault
303
+ inner_vault: (
304
+ ChecksumAddress | None
305
+ ) # for MorphoVaultV2Adapter: the underlying market vault
304
306
 
305
307
 
306
308
  @dataclass(slots=True)
307
309
  class VaultV2Info:
308
310
  """Snapshot of a Morpho Vault V2."""
309
311
 
310
- address: str
312
+ address: ChecksumAddress
311
313
  name: str
312
314
  symbol: str
313
- asset_address: str
315
+ asset_address: ChecksumAddress
314
316
  asset_symbol: str
315
317
  asset_decimals: int
316
318
  total_assets: int
@@ -319,13 +321,13 @@ class VaultV2Info:
319
321
  share_price: float
320
322
  max_apy: float
321
323
  performance_fee: float
322
- performance_fee_recipient: str
324
+ performance_fee_recipient: ChecksumAddress | None
323
325
  management_fee: float
324
- management_fee_recipient: str
325
- owner: str
326
- curator: str
327
- allocators: list[str]
328
- sentinels: list[str]
326
+ management_fee_recipient: ChecksumAddress | None
327
+ owner: ChecksumAddress
328
+ curator: ChecksumAddress | None
329
+ allocators: list[ChecksumAddress]
330
+ sentinels: list[ChecksumAddress]
329
331
  liquidity_adapter: VaultV2Adapter | None
330
332
  adapters: list[VaultV2Adapter]
331
333
  caps: list[VaultV2Cap] = field(default_factory=list)
@@ -355,22 +357,24 @@ class VaultV1MarketAllocation:
355
357
  class VaultV1Info:
356
358
  """Snapshot of a MetaMorpho V1 vault."""
357
359
 
358
- address: str
360
+ address: ChecksumAddress
359
361
  name: str
360
362
  symbol: str
361
- asset_address: str
363
+ asset_address: ChecksumAddress
362
364
  asset_symbol: str
363
365
  asset_decimals: int
364
366
  total_assets: int
365
367
  fee_wad: int
366
- owner: str
367
- curator: str
368
- guardian: str
369
- fee_recipient: str
370
- allocators: list[str]
368
+ owner: ChecksumAddress
369
+ curator: ChecksumAddress | None
370
+ guardian: ChecksumAddress | None
371
+ fee_recipient: ChecksumAddress | None
372
+ allocators: list[ChecksumAddress]
371
373
  allocations: list[VaultV1MarketAllocation]
372
374
  public_allocator: VaultFlowCap | None # vault-level (no flowCap filtering)
373
- public_allocator_flow_caps: dict[str, tuple[int, int]] # marketId -> (maxIn,maxOut)
375
+ public_allocator_flow_caps: dict[
376
+ MorphoBlueMarketId, tuple[int, int]
377
+ ] # (maxIn, maxOut)
374
378
 
375
379
 
376
380
  def fetch_vault(
@@ -431,10 +435,10 @@ def _parse_vault_v2(raw: dict) -> VaultV2Info:
431
435
  ]
432
436
  caps = [_parse_v2_cap(c) for c in (raw.get("caps") or {}).get("items", []) if c]
433
437
  return VaultV2Info(
434
- address=raw["address"],
438
+ address=_api_address(raw.get("address")),
435
439
  name=raw.get("name") or "",
436
440
  symbol=raw.get("symbol") or "",
437
- asset_address=asset.get("address") or "",
441
+ asset_address=_api_address(asset.get("address")),
438
442
  asset_symbol=asset.get("symbol") or "",
439
443
  asset_decimals=int(asset.get("decimals") or 0),
440
444
  total_assets=int(raw.get("totalAssets") or 0),
@@ -443,18 +447,22 @@ def _parse_vault_v2(raw: dict) -> VaultV2Info:
443
447
  share_price=float(raw.get("sharePrice") or 0.0),
444
448
  max_apy=float(raw.get("maxApy") or 0.0),
445
449
  performance_fee=float(raw.get("performanceFee") or 0.0),
446
- performance_fee_recipient=raw.get("performanceFeeRecipient") or "",
450
+ performance_fee_recipient=_api_optional_address(
451
+ raw.get("performanceFeeRecipient")
452
+ ),
447
453
  management_fee=float(raw.get("managementFee") or 0.0),
448
- management_fee_recipient=raw.get("managementFeeRecipient") or "",
449
- owner=(raw.get("owner") or {}).get("address") or "",
450
- curator=(raw.get("curator") or {}).get("address") or "",
454
+ management_fee_recipient=_api_optional_address(
455
+ raw.get("managementFeeRecipient")
456
+ ),
457
+ owner=_api_address((raw.get("owner") or {}).get("address")),
458
+ curator=_api_optional_address((raw.get("curator") or {}).get("address")),
451
459
  allocators=[
452
- a["allocator"]["address"]
460
+ _api_address(a["allocator"]["address"])
453
461
  for a in raw.get("allocators") or []
454
462
  if (a.get("allocator") or {}).get("address")
455
463
  ],
456
464
  sentinels=[
457
- s["sentinel"]["address"]
465
+ _api_address(s["sentinel"]["address"])
458
466
  for s in raw.get("sentinels") or []
459
467
  if (s.get("sentinel") or {}).get("address")
460
468
  ],
@@ -466,10 +474,10 @@ def _parse_vault_v2(raw: dict) -> VaultV2Info:
466
474
 
467
475
  def _parse_v2_adapter(raw: dict) -> VaultV2Adapter:
468
476
  return VaultV2Adapter(
469
- address=raw.get("address") or "",
477
+ address=_api_address(raw.get("address")),
470
478
  adapter_type=raw.get("type") or raw.get("__typename") or "",
471
479
  assets=int(raw.get("assets") or 0),
472
- inner_vault=(raw.get("metaMorpho") or {}).get("address"),
480
+ inner_vault=_api_optional_address((raw.get("metaMorpho") or {}).get("address")),
473
481
  )
474
482
 
475
483
 
@@ -500,7 +508,10 @@ def _parse_v2_cap(raw: dict) -> VaultV2Cap:
500
508
 
501
509
  def _decode_market_v1_id_data(
502
510
  id_data: str,
503
- ) -> tuple[str, str, str, str, int] | None:
511
+ ) -> (
512
+ tuple[ChecksumAddress, ChecksumAddress, ChecksumAddress, ChecksumAddress, int]
513
+ | None
514
+ ):
504
515
  """Decode a Vault V2 `MarketV1`-type cap idData into MarketParams.
505
516
 
506
517
  Vault V2 encodes the cap idData as `abi.encode(string idType, address adapter,
@@ -548,6 +559,16 @@ def _decode_market_v1_id_data(
548
559
  )
549
560
 
550
561
 
562
+ def _api_address(value: object) -> ChecksumAddress:
563
+ if not isinstance(value, str) or not Web3.is_address(value):
564
+ raise MorphoApiError(f"Morpho API returned an invalid address: {value!r}")
565
+ return Web3.to_checksum_address(value)
566
+
567
+
568
+ def _api_optional_address(value: object) -> ChecksumAddress | None:
569
+ return None if value is None or value == "" else _api_address(value)
570
+
571
+
551
572
  def _api_market_id(value: object) -> MorphoBlueMarketId:
552
573
  try:
553
574
  return parse_morpho_market_id(str(value or ""))
@@ -598,7 +619,7 @@ def _parse_vault_v1(raw: dict) -> VaultV1Info:
598
619
 
599
620
  pac = raw.get("publicAllocatorConfig")
600
621
  flow_cap_summary: VaultFlowCap | None = None
601
- flow_caps: dict[str, tuple[int, int]] = {}
622
+ flow_caps: dict[MorphoBlueMarketId, tuple[int, int]] = {}
602
623
  if pac:
603
624
  flow_cap_summary = VaultFlowCap(
604
625
  fee_wei=int(pac.get("fee") or 0),
@@ -607,25 +628,29 @@ def _parse_vault_v1(raw: dict) -> VaultV1Info:
607
628
  admin=pac.get("admin"),
608
629
  )
609
630
  for fc in pac.get("flowCaps") or []:
610
- mid = ((fc.get("market") or {}).get("marketId") or "").lower()
611
- if mid:
612
- flow_caps[mid] = (int(fc.get("maxIn") or 0), int(fc.get("maxOut") or 0))
631
+ if raw_mid := (fc.get("market") or {}).get("marketId"):
632
+ flow_caps[_api_market_id(raw_mid)] = (
633
+ int(fc.get("maxIn") or 0),
634
+ int(fc.get("maxOut") or 0),
635
+ )
613
636
 
614
637
  return VaultV1Info(
615
- address=raw["address"],
638
+ address=_api_address(raw.get("address")),
616
639
  name=raw.get("name") or "",
617
640
  symbol=raw.get("symbol") or "",
618
- asset_address=asset.get("address") or "",
641
+ asset_address=_api_address(asset.get("address")),
619
642
  asset_symbol=asset.get("symbol") or "",
620
643
  asset_decimals=int(asset.get("decimals") or 0),
621
644
  total_assets=int(state.get("totalAssets") or 0),
622
645
  fee_wad=int(state.get("fee") or 0),
623
- owner=state.get("owner") or "",
624
- curator=state.get("curator") or "",
625
- guardian=state.get("guardian") or "",
626
- fee_recipient=state.get("feeRecipient") or "",
646
+ owner=_api_address(state.get("owner")),
647
+ curator=_api_optional_address(state.get("curator")),
648
+ guardian=_api_optional_address(state.get("guardian")),
649
+ fee_recipient=_api_optional_address(state.get("feeRecipient")),
627
650
  allocators=[
628
- a["address"] for a in raw.get("allocators") or [] if a.get("address")
651
+ _api_address(a["address"])
652
+ for a in raw.get("allocators") or []
653
+ if a.get("address")
629
654
  ],
630
655
  allocations=allocations,
631
656
  public_allocator=flow_cap_summary,
@@ -633,27 +658,28 @@ def _parse_vault_v1(raw: dict) -> VaultV1Info:
633
658
  )
634
659
 
635
660
 
661
+ def _is_market(entry: dict, market_id: MorphoBlueMarketId) -> bool:
662
+ """Whether an allocation or flow-cap ``entry`` names ``market_id``, in any
663
+ spelling the API returns."""
664
+ raw = (entry.get("market") or {}).get("marketId")
665
+ try:
666
+ return parse_morpho_market_id(str(raw or "")) == market_id
667
+ except ValueError:
668
+ return False
669
+
670
+
636
671
  def _parse_vault(raw: dict, market_id: MorphoBlueMarketId) -> VaultAllocation:
637
672
  asset = raw.get("asset") or {}
638
673
  state = raw.get("state") or {}
639
674
  allocation_for_market: dict = next(
640
- (
641
- a
642
- for a in (state.get("allocation") or [])
643
- if (a.get("market") or {}).get("marketId", "").lower() == market_id
644
- ),
675
+ (a for a in (state.get("allocation") or []) if _is_market(a, market_id)),
645
676
  {},
646
677
  )
647
678
  pac = raw.get("publicAllocatorConfig")
648
679
  flow_cap: VaultFlowCap | None = None
649
680
  if pac:
650
681
  flow_for_market = next(
651
- (
652
- fc
653
- for fc in (pac.get("flowCaps") or [])
654
- if (fc.get("market") or {}).get("marketId", "").lower()
655
- == market_id.lower()
656
- ),
682
+ (fc for fc in (pac.get("flowCaps") or []) if _is_market(fc, market_id)),
657
683
  None,
658
684
  )
659
685
  if flow_for_market:
@@ -667,7 +693,7 @@ def _parse_vault(raw: dict, market_id: MorphoBlueMarketId) -> VaultAllocation:
667
693
  a["address"] for a in (raw.get("allocators") or []) if a.get("address")
668
694
  ]
669
695
  return VaultAllocation(
670
- vault_address=raw["address"],
696
+ vault_address=_api_address(raw["address"]),
671
697
  vault_name=raw.get("name") or "",
672
698
  vault_symbol=raw.get("symbol") or "",
673
699
  asset_symbol=asset.get("symbol") or "",
@@ -98,7 +98,7 @@ class VaultState(Protocol):
98
98
  @property
99
99
  def block_timestamp(self) -> int: ...
100
100
  @property
101
- def asset(self) -> str: ...
101
+ def asset(self) -> ChecksumAddress: ...
102
102
  @property
103
103
  def asset_symbol(self) -> str: ...
104
104
  @property
@@ -219,11 +219,11 @@ class VaultValidation:
219
219
  found. Raw amounts are in the underlying ``asset``'s decimals.
220
220
  """
221
221
 
222
- vault: str
222
+ vault: ChecksumAddress
223
223
  chain_id: ChainId
224
224
  block_number: int
225
225
  block_timestamp: int
226
- asset: str
226
+ asset: ChecksumAddress
227
227
  asset_symbol: str
228
228
  asset_decimals: int
229
229
  asset_price_usd: float | None
@@ -164,7 +164,7 @@ class VaultData:
164
164
  total_assets: int
165
165
  total_supply: int
166
166
  supply_cap: int
167
- asset: str
167
+ asset: ChecksumAddress
168
168
  asset_symbol: str
169
169
  access_manager: str
170
170
  price_oracle_addr: str
@@ -505,7 +505,7 @@ def _morpho_breakdown_json(
505
505
  prices = data.token_prices_usd
506
506
  return [
507
507
  {
508
- "morpho_market_id": "0x" + str(pb.market_id),
508
+ "morpho_market_id": pb.market_id,
509
509
  "collateral_symbol": resolve_token_symbol(ctx, pb.collateral_token) or None,
510
510
  "loan_symbol": resolve_token_symbol(ctx, pb.loan_token) or None,
511
511
  "collateral": _build_breakdown_amount_json(
@@ -63,7 +63,11 @@ from ipor_fusion.readers.lending_health import (
63
63
  )
64
64
  from ipor_fusion.readers.morpho import MORPHO_BLUE_ADDRESSES, MorphoPositionBreakdown
65
65
  from ipor_fusion.substrates import decode_substrate, market_name
66
- from ipor_fusion.types import Amount, MarketId, MorphoBlueMarketId
66
+ from ipor_fusion.types import (
67
+ Amount,
68
+ MarketId,
69
+ parse_morpho_market_id,
70
+ )
67
71
  from ipor_fusion.validation import Erc20Holdings
68
72
  from ipor_fusion.vault_data import (
69
73
  FeeData,
@@ -86,6 +90,7 @@ from ipor_fusion.vault_info import (
86
90
  _build_dependency_graph_json,
87
91
  _build_fees_json,
88
92
  _build_share_price_json,
93
+ _morpho_breakdown_json,
89
94
  _partition_balance_fuses,
90
95
  )
91
96
 
@@ -1509,7 +1514,7 @@ _TOKEN_WETH = Web3.to_checksum_address("0x" + "33" * 20)
1509
1514
 
1510
1515
  def _morpho_breakdown(market_id: str = "ab" * 32):
1511
1516
  return MorphoPositionBreakdown(
1512
- market_id=MorphoBlueMarketId(market_id),
1517
+ market_id=parse_morpho_market_id(market_id),
1513
1518
  loan_token=_TOKEN_USDC,
1514
1519
  collateral_token=_TOKEN_WETH,
1515
1520
  collateral=Amount(10 * 10**18),
@@ -1705,6 +1710,30 @@ class TestFetcherCollectors:
1705
1710
  assert 14 in out and 1 not in out
1706
1711
  assert len(out[14]) == 1
1707
1712
 
1713
+ @patch("ipor_fusion.vault_info.resolve_token_decimals", return_value=18)
1714
+ @patch("ipor_fusion.vault_info.resolve_token_symbol", return_value="WETH")
1715
+ @patch("ipor_fusion.vault_data.resolve_token_decimals", return_value=18)
1716
+ @patch("ipor_fusion.vault_data.resolve_token_symbol", return_value="WETH")
1717
+ def test_collected_morpho_id_renders_with_one_prefix(
1718
+ self, _sym, _dec, _view_sym, _view_dec, capsys
1719
+ ):
1720
+ (market_id,) = _collect_morpho_substrates({14: [bytes.fromhex("ab" * 32)]})[14]
1721
+ breakdown = MorphoPositionBreakdown(
1722
+ market_id=market_id,
1723
+ loan_token=_TOKEN_USDC,
1724
+ collateral_token=_TOKEN_WETH,
1725
+ collateral=Amount(0),
1726
+ borrow_assets=Amount(0),
1727
+ supply_assets=Amount(0),
1728
+ )
1729
+ data = _make_data(morpho_positions={14: [breakdown]})
1730
+ (entry,) = _morpho_breakdown_json(MagicMock(), data, [breakdown])
1731
+ assert entry["morpho_market_id"] == "0x" + "ab" * 32
1732
+ _print_lending_health(MagicMock(), data)
1733
+ out = capsys.readouterr().out
1734
+ assert f"morpho market 0x{'ab' * 32} " in out
1735
+ assert "0x0x" not in out
1736
+
1708
1737
  def test_collect_morpho_substrates_skips_wrong_length(self):
1709
1738
  bad = bytes.fromhex("aa" * 16) # wrong length
1710
1739
  out = _collect_morpho_substrates({14: [bad]})
@@ -21,6 +21,7 @@ from ipor_fusion.morpho_api import (
21
21
  fetch_market,
22
22
  fetch_vault,
23
23
  )
24
+ from ipor_fusion.types import parse_morpho_market_id
24
25
 
25
26
  MARKET_ID = "ad656d430bb3d8c1469bf45c8ad4ebae1b04be04757c69fa424eec78d7b3f4dc"
26
27
  LOAN_TOKEN = Web3.to_checksum_address("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
@@ -381,6 +382,60 @@ class TestMorphoApiClient:
381
382
  assert vault.flow_cap.admin == ADMIN_ADDR
382
383
  assert vault.allocators == [PUBLIC_ALLOCATOR_ETH, ADMIN_ADDR]
383
384
 
385
+ def test_fetch_market_matches_allocations_in_any_spelling(self):
386
+ upper = "0x" + MARKET_ID.upper()
387
+ market = {"market": {"marketId": upper}}
388
+ body = {
389
+ "data": {
390
+ "marketById": {
391
+ "marketId": upper,
392
+ "lltv": "915000000000000000",
393
+ "loanAsset": {
394
+ "address": LOAN_TOKEN,
395
+ "symbol": "USDC",
396
+ "decimals": 6,
397
+ },
398
+ "collateralAsset": {
399
+ "address": COLLATERAL_TOKEN,
400
+ "symbol": "WOUSD",
401
+ "decimals": 18,
402
+ },
403
+ "oracle": {"address": ORACLE},
404
+ "irmAddress": IRM,
405
+ "state": {},
406
+ "supplyingVaults": [
407
+ {
408
+ "address": VAULT_ADDR,
409
+ "symbol": "v",
410
+ "name": "v",
411
+ "asset": {"symbol": "USDC", "decimals": 6},
412
+ "state": {
413
+ "allocation": [
414
+ {**market, "supplyAssets": 123, "supplyCap": 456}
415
+ ]
416
+ },
417
+ "publicAllocatorConfig": {
418
+ "fee": 0,
419
+ "flowCaps": [{**market, "maxIn": 7, "maxOut": 8}],
420
+ },
421
+ }
422
+ ],
423
+ }
424
+ }
425
+ }
426
+ with patch("ipor_fusion.morpho_api.urlopen") as mock_open:
427
+ mock_resp = MagicMock()
428
+ mock_resp.read.return_value = json.dumps(body).encode()
429
+ mock_open.return_value.__enter__.return_value = mock_resp
430
+
431
+ result = fetch_market(parse_morpho_market_id(MARKET_ID), 1)
432
+
433
+ assert result.market_id == "0x" + MARKET_ID.lower()
434
+ vault = result.vaults[0]
435
+ assert (vault.supply_assets, vault.supply_cap) == (123, 456)
436
+ assert vault.flow_cap is not None
437
+ assert (vault.flow_cap.max_in, vault.flow_cap.max_out) == (7, 8)
438
+
384
439
  def test_fetch_market_handles_missing_market(self):
385
440
  with patch("ipor_fusion.morpho_api.urlopen") as mock_open:
386
441
  mock_resp = MagicMock()