ipor-fusion 4.1.0.dev26__tar.gz → 4.1.0.dev38__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 (361) hide show
  1. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/PKG-INFO +1 -1
  2. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/pyproject.toml +1 -1
  3. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/options.py +16 -20
  4. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/fusion_factory.py +4 -3
  5. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/mcp/server.py +16 -6
  6. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/oracle_mapping.py +26 -9
  7. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_data.py +21 -3
  8. ipor_fusion-4.1.0.dev38/tests/_addresses.py +17 -0
  9. ipor_fusion-4.1.0.dev38/tests/test_addresses_helper.py +20 -0
  10. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_commands.py +36 -9
  11. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_helpers.py +15 -12
  12. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_fusion_instance.py +3 -0
  13. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_preflight.py +9 -0
  14. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_server.py +15 -5
  15. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_oracle_mapping.py +20 -0
  16. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_types_and_events.py +2 -3
  17. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_data_block.py +11 -1
  18. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.claude-plugin/marketplace.json +0 -0
  19. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.claude-plugin/plugin.json +0 -0
  20. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.env.example +0 -0
  21. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.gemini/settings.json +0 -0
  22. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.git-blame-ignore-revs +0 -0
  23. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/cd.yml +0 -0
  24. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/ci.yml +0 -0
  25. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/commit-lint.yml +0 -0
  26. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/notify-slack.yml +0 -0
  27. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/python-build.yml +0 -0
  28. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/python-publish.yml +0 -0
  29. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/release.yml +0 -0
  30. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.github/workflows/report-slack.yml +0 -0
  31. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.gitignore +0 -0
  32. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.mcp.json +0 -0
  33. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.pre-commit-config.yaml +0 -0
  34. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/.python-version +0 -0
  35. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/AGENTS.md +0 -0
  36. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/CHANGELOG.md +0 -0
  37. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/CLAUDE.md +0 -0
  38. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/CODEOWNERS +0 -0
  39. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/LICENSE +0 -0
  40. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/MIGRATION_4.md +0 -0
  41. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/README.md +0 -0
  42. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/build.sh +0 -0
  43. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/CONTRIBUTING.md +0 -0
  44. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/README.md +0 -0
  45. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  46. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  47. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  48. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/external_state_margin_leg_base.py +0 -0
  49. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/hypercore_vault_official_deployment_hyperevm.py +0 -0
  50. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/examples/simple_aave_v3_supply_base.py +0 -0
  51. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/pytest.ini +0 -0
  52. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/scripts/commit-check.sh +0 -0
  53. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/scripts/fusion_error_signatures.py +0 -0
  54. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/skills/ipor-deploy-vault/SKILL.md +0 -0
  55. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/__init__.py +0 -0
  56. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/about.py +0 -0
  57. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cache.py +0 -0
  58. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/chains.py +0 -0
  59. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/__init__.py +0 -0
  60. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/address_cmd.py +0 -0
  61. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  62. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/config_cmd.py +0 -0
  63. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/config_store.py +0 -0
  64. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/main.py +0 -0
  65. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/market_cmd.py +0 -0
  66. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  67. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/preflight_cmd.py +0 -0
  68. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  69. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/vault_health.py +0 -0
  70. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  71. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  72. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  73. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/config/__init__.py +0 -0
  74. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/config/roles.py +0 -0
  75. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/__init__.py +0 -0
  76. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/access.py +0 -0
  77. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/address.py +0 -0
  78. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/context.py +0 -0
  79. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/context_manager.py +0 -0
  80. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/contract.py +0 -0
  81. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/erc20.py +0 -0
  82. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/external_state_executor.py +0 -0
  83. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/fee_manager.py +0 -0
  84. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/fuse.py +0 -0
  85. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/fuse_action.py +0 -0
  86. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/fuse_decoding.py +0 -0
  87. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/logs.py +0 -0
  88. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/multicall.py +0 -0
  89. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/oracle.py +0 -0
  90. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/plasma_vault.py +0 -0
  91. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/rewards_manager.py +0 -0
  92. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/simulation.py +0 -0
  93. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/simulation_result.py +0 -0
  94. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  95. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/__init__.py +0 -0
  96. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  97. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  98. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  99. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  100. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  101. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  102. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  103. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  104. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  105. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/contracts.py +0 -0
  106. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/discovery.py +0 -0
  107. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/errors.py +0 -0
  108. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  109. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  110. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  111. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  112. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/lane.py +0 -0
  113. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/logs.py +0 -0
  114. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/messages.py +0 -0
  115. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/simulation.py +0 -0
  116. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  117. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  118. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  119. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  120. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  121. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/crosschain/transport.py +0 -0
  122. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/dependency_graph.py +0 -0
  123. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/entry_cli.py +0 -0
  124. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/entry_mcp.py +0 -0
  125. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/errors.py +0 -0
  126. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/explorer.py +0 -0
  127. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/field_docs.py +0 -0
  128. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/__init__.py +0 -0
  129. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  130. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/async_action.py +0 -0
  131. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/base.py +0 -0
  132. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  133. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/erc4626.py +0 -0
  134. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  135. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/events.py +0 -0
  136. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/external_state.py +0 -0
  137. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
  138. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  139. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  140. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/hypercore.py +0 -0
  141. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
  142. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/hypercore_pre_hooks.py +0 -0
  143. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/merkl.py +0 -0
  144. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/morpho.py +0 -0
  145. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  146. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  147. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fuses/universal.py +0 -0
  148. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/fusion_errors.py +0 -0
  149. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/guide/__init__.py +0 -0
  150. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/guide/architecture.md +0 -0
  151. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/guide/glossary.md +0 -0
  152. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/guide/invariants.md +0 -0
  153. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/guide/quickstart.md +0 -0
  154. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/hypercore/__init__.py +0 -0
  155. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/market_ids.py +0 -0
  156. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/market_info.py +0 -0
  157. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/mcp/__init__.py +0 -0
  158. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/mcp/guide.py +0 -0
  159. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/mcp/models.py +0 -0
  160. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/morpho_api.py +0 -0
  161. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/preflight.py +0 -0
  162. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/py.typed +0 -0
  163. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/__init__.py +0 -0
  164. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/aave_v3.py +0 -0
  165. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/aave_v4.py +0 -0
  166. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/compound_v3.py +0 -0
  167. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/euler_v2.py +0 -0
  168. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/hypercore.py +0 -0
  169. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  170. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/lending_health.py +0 -0
  171. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/morpho.py +0 -0
  172. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  173. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  174. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/position_manager.py +0 -0
  175. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  176. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  177. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/readers/unpriceable.py +0 -0
  178. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/registry.py +0 -0
  179. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/substrates.py +0 -0
  180. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/types.py +0 -0
  181. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/validation.py +0 -0
  182. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_erc20.py +0 -0
  183. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_info.py +0 -0
  184. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_managers.py +0 -0
  185. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_positions.py +0 -0
  186. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/src/ipor_fusion/vault_reads.py +0 -0
  187. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/UniswapV3UniversalRouter.py +0 -0
  188. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_crosschain.py +0 -0
  189. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_crosschain_lifecycle.py +0 -0
  190. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_crosschain_recovery.py +0 -0
  191. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_euler_v2.py +0 -0
  192. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_event_args.py +0 -0
  193. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_fake_rpc.py +0 -0
  194. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_foundry.py +0 -0
  195. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_golden.py +0 -0
  196. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_guide.py +0 -0
  197. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_hypercore_model.py +0 -0
  198. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_hypercore_shadow.py +0 -0
  199. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_live.py +0 -0
  200. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_multicall.py +0 -0
  201. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_oracle_mapping_fake.py +0 -0
  202. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_simulate.py +0 -0
  203. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/_validation.py +0 -0
  204. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/addresses.py +0 -0
  205. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/conftest.py +0 -0
  206. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/constants.py +0 -0
  207. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  208. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/hypercore_market55.json +0 -0
  209. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/hypercore_official.json +0 -0
  210. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/meta_morpho_v1.json +0 -0
  211. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +0 -0
  212. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/meta_morpho_v2.json +0 -0
  213. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +0 -0
  214. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_api/market.json +0 -0
  215. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_api/vault_v1.json +0 -0
  216. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_api/vault_v2.json +0 -0
  217. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +0 -0
  218. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue.json +0 -0
  219. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue.mcp.json +0 -0
  220. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue_api_error.json +0 -0
  221. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +0 -0
  222. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue_no_api.json +0 -0
  223. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +0 -0
  224. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/oracle_mapping_golden/mapping.json +0 -0
  225. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +0 -0
  226. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/simulate_v1_request.json +0 -0
  227. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/clean.json +0 -0
  228. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/clean.mcp.json +0 -0
  229. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  230. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/deployment_error.json +0 -0
  231. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/deployment_error.mcp.json +0 -0
  232. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/deployment_error.txt +0 -0
  233. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/fetched.cli.json +0 -0
  234. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/fetched.json +0 -0
  235. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/fetched.mcp.json +0 -0
  236. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/full.json +0 -0
  237. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/full.mcp.json +0 -0
  238. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/full.txt +0 -0
  239. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/rich.json +0 -0
  240. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/rich.mcp.json +0 -0
  241. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  242. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_about.py +0 -0
  243. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_access_manager.py +0 -0
  244. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cache.py +0 -0
  245. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_chains.py +0 -0
  246. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_address.py +0 -0
  247. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_changelog.py +0 -0
  248. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_config_store.py +0 -0
  249. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_hypercore.py +0 -0
  250. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_lending_health.py +0 -0
  251. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_market_golden.py +0 -0
  252. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_market_morpho_blue.py +0 -0
  253. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_mcp_parity.py +0 -0
  254. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_oracle_mapping.py +0 -0
  255. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_oracle_mapping_golden.py +0 -0
  256. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_pending_requests_scan.py +0 -0
  257. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_preflight.py +0 -0
  258. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_role_accounts.py +0 -0
  259. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_vault_info_golden.py +0 -0
  260. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_vault_predates_getters.py +0 -0
  261. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_cli_vault_validate.py +0 -0
  262. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_contract_call.py +0 -0
  263. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_contract_encoder.py +0 -0
  264. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_ccip_events.py +0 -0
  265. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_ccip_indexer.py +0 -0
  266. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_ccip_quote.py +0 -0
  267. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_chainlink.py +0 -0
  268. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_contracts.py +0 -0
  269. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_encoding.py +0 -0
  270. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_errors.py +0 -0
  271. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_lane.py +0 -0
  272. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_layering.py +0 -0
  273. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_crosschain_transport.py +0 -0
  274. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_docs_typing.py +0 -0
  275. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_erc20.py +0 -0
  276. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_error_hierarchy.py +0 -0
  277. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_examples_crosschain.py +0 -0
  278. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  279. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_examples_hypercore_hyperevm.py +0 -0
  280. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_examples_vaults.py +0 -0
  281. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_explorer.py +0 -0
  282. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_external_state_events.py +0 -0
  283. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_external_state_executor.py +0 -0
  284. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_fee_manager.py +0 -0
  285. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_fuse_decoding.py +0 -0
  286. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_fuse_encoding.py +0 -0
  287. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_fusion_errors.py +0 -0
  288. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_governance_wrappers.py +0 -0
  289. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_guide.py +0 -0
  290. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_deployment.py +0 -0
  291. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_events.py +0 -0
  292. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_flow.py +0 -0
  293. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_live.py +0 -0
  294. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_outcome.py +0 -0
  295. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_pre_hooks.py +0 -0
  296. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_readers.py +0 -0
  297. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_hypercore_simulation.py +0 -0
  298. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_logs.py +0 -0
  299. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_address.py +0 -0
  300. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_hypercore.py +0 -0
  301. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_models.py +0 -0
  302. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_mcp_vault_validate.py +0 -0
  303. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_multicall.py +0 -0
  304. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_oracle.py +0 -0
  305. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_plasma_vault.py +0 -0
  306. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_preflight.py +0 -0
  307. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_public_api.py +0 -0
  308. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_public_type_reachability.py +0 -0
  309. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_reader_encoding.py +0 -0
  310. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_registry.py +0 -0
  311. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_revert_decoding.py +0 -0
  312. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_rewards_manager.py +0 -0
  313. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_roles.py +0 -0
  314. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_sdk_layering.py +0 -0
  315. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  316. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  317. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  318. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  319. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  320. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  321. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  322. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  323. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  324. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_examples_arbitrum.py +0 -0
  325. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_examples_base.py +0 -0
  326. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_examples_crosschain.py +0 -0
  327. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_examples_hyperevm.py +0 -0
  328. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_external_state_executor_base.py +0 -0
  329. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  330. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  331. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  332. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_morpho_blue.py +0 -0
  333. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_pilot_v3.py +0 -0
  334. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  335. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  336. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  337. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  338. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_preflight_base.py +0 -0
  339. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_readers_integration.py +0 -0
  340. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_release_funds.py +0 -0
  341. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_swap_on_base.py +0 -0
  342. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_timelock_base.py +0 -0
  343. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  344. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulation_errors.py +0 -0
  345. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_simulation_observation.py +0 -0
  346. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_solidity_compiler.py +0 -0
  347. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_solidity_mirrors.py +0 -0
  348. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_substrates.py +0 -0
  349. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_unpriceable_tokens.py +0 -0
  350. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_validation.py +0 -0
  351. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_dep_graph.py +0 -0
  352. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_info_fetched_golden.py +0 -0
  353. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_info_onchain_base.py +0 -0
  354. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  355. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_reconciliation_onchain_base.py +0 -0
  356. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_vault_simulator_blocks.py +0 -0
  357. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_web3_context.py +0 -0
  358. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/test_withdraw_manager.py +0 -0
  359. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/typed_client/consumer.py +0 -0
  360. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/tests/typed_client/rejected.py +0 -0
  361. {ipor_fusion-4.1.0.dev26 → ipor_fusion-4.1.0.dev38}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.1.0.dev26
3
+ Version: 4.1.0.dev38
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.1.0.dev26"
3
+ version = "4.1.0.dev38"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import click
4
4
  from eth_typing import ChecksumAddress
5
- from web3 import Web3
6
5
 
7
6
  from ipor_fusion.chains import CHAIN_NAME_TO_ID, ensure_supported_chain
8
7
  from ipor_fusion.cli.config_store import (
@@ -13,7 +12,12 @@ from ipor_fusion.core.context import Web3Context
13
12
  from ipor_fusion.errors import (
14
13
  UnsupportedChainError,
15
14
  )
16
- from ipor_fusion.types import ChainId, PlasmaVaultAddress
15
+ from ipor_fusion.types import (
16
+ ChainId,
17
+ PlasmaVaultAddress,
18
+ parse_checksum_address,
19
+ parse_hex_address,
20
+ )
17
21
 
18
22
 
19
23
  # click < 8.4 does not make ParamType generic, so it cannot be subscripted.
@@ -27,26 +31,18 @@ class AddressType(click.ParamType): # pyright: ignore[reportMissingTypeArgument
27
31
  self.fail(f"expected string, got {type(value).__name__}", param, ctx)
28
32
 
29
33
  value = value.strip()
30
- if not value.startswith("0x"):
31
- value = "0x" + value
34
+ if value[:2] in ("0x", "0X"):
35
+ value = value[2:]
36
+ value = "0x" + value
32
37
 
33
- raw = value[2:]
34
- if len(raw) != 40 or not all(c in "0123456789abcdefABCDEF" for c in raw):
38
+ try:
39
+ parse_hex_address(value)
40
+ except ValueError:
35
41
  self.fail(f"invalid Ethereum address: {value}", param, ctx)
36
-
37
- if not raw.islower() and not raw.isupper():
38
- try:
39
- checksum = Web3.to_checksum_address(value)
40
- except Exception:
41
- self.fail(f"invalid address checksum: {value}", param, ctx)
42
- if checksum != value:
43
- self.fail(
44
- f"bad checksum — expected {checksum}, got {value}",
45
- param,
46
- ctx,
47
- )
48
-
49
- return Web3.to_checksum_address(value)
42
+ try:
43
+ return parse_checksum_address(value)
44
+ except ValueError as exc:
45
+ self.fail(str(exc), param, ctx)
50
46
 
51
47
 
52
48
  ADDRESS = AddressType()
@@ -38,6 +38,7 @@ from ipor_fusion.types import (
38
38
  Period,
39
39
  PlasmaVaultAddress,
40
40
  parse_access_manager_address,
41
+ parse_checksum_address,
41
42
  parse_plasma_vault_address,
42
43
  )
43
44
 
@@ -82,8 +83,8 @@ class FusionInstance:
82
83
  def from_dict(cls, data: Mapping[str, object]) -> FusionInstance:
83
84
  """Rebuild an instance from its JSON form (``dataclasses.asdict``),
84
85
  parsing every field: role addresses through their parsers, the rest
85
- checksummed, integers and strings type-checked. Raises ``ValueError``
86
- on a missing, extra or malformed field."""
86
+ through `parse_checksum_address`, integers and strings type-checked.
87
+ Raises ``ValueError`` on a missing, extra or malformed field."""
87
88
  names = {f.name for f in fields(cls)}
88
89
  missing = names - data.keys()
89
90
  extra = data.keys() - names
@@ -128,7 +129,7 @@ def _text(value: object) -> str:
128
129
 
129
130
 
130
131
  def _address(value: object) -> ChecksumAddress:
131
- return Web3.to_checksum_address(_text(value))
132
+ return parse_checksum_address(_text(value))
132
133
 
133
134
 
134
135
  _FUSION_INSTANCE_OUTPUT_TYPES: list[str] = [
@@ -8,6 +8,7 @@ from collections.abc import Callable
8
8
  from functools import partial
9
9
  from typing import Annotated, Literal
10
10
 
11
+ from eth_typing import ChecksumAddress
11
12
  from mcp.server.fastmcp import FastMCP
12
13
  from pydantic import AfterValidator, BeforeValidator, Field
13
14
  from web3 import Web3
@@ -76,8 +77,8 @@ from ipor_fusion.registry import (
76
77
  from ipor_fusion.types import (
77
78
  ChainId,
78
79
  PlasmaVaultAddress,
80
+ parse_checksum_address,
79
81
  parse_morpho_market_id,
80
- parse_plasma_vault_address,
81
82
  )
82
83
  from ipor_fusion.validation import validate_vault
83
84
  from ipor_fusion.vault_data import (
@@ -117,14 +118,19 @@ def _resolve_provider(cfg: FusionConfig, chain_id: ChainId) -> str:
117
118
  )
118
119
 
119
120
 
120
- def _vault_arg(raw: str) -> PlasmaVaultAddress:
121
- """A vault address as a tool caller sends it: surrounding whitespace and a
121
+ def _address_arg(raw: str) -> ChecksumAddress:
122
+ """An address as a tool caller sends it: surrounding whitespace and a
122
123
  missing ``0x`` are accepted, as in 3.x; letter case follows
123
124
  `parse_checksum_address`, so a wrong EIP-55 checksum is refused."""
124
125
  value = raw.strip()
125
126
  if value[:2] not in ("0x", "0X"):
126
127
  value = "0x" + value
127
- return parse_plasma_vault_address(value)
128
+ return parse_checksum_address(value)
129
+
130
+
131
+ def _vault_arg(raw: str) -> PlasmaVaultAddress:
132
+ """`_address_arg`, labeled as a Plasma Vault's address."""
133
+ return PlasmaVaultAddress(_address_arg(raw))
128
134
 
129
135
 
130
136
  def _resolve_chain_id(
@@ -341,6 +347,10 @@ def vault_preflight(
341
347
  raise ValueError(f"calldata: {exc}") from exc
342
348
  if not actions:
343
349
  raise ValueError("calldata: the batch has no actions")
350
+ try:
351
+ sender_address = _address_arg(sender)
352
+ except ValueError as exc:
353
+ raise ValueError(f"sender: {exc}") from exc
344
354
  cfg = load_config()
345
355
  checksum = _vault_arg(vault_address)
346
356
  chain_id = _resolve_chain_id(cfg, checksum, chain_id)
@@ -350,7 +360,7 @@ def vault_preflight(
350
360
  ctx,
351
361
  checksum,
352
362
  actions,
353
- sender=Web3.to_checksum_address(sender),
363
+ sender=sender_address,
354
364
  simulate=simulate,
355
365
  )
356
366
  return ExecutePreflightResponse.model_validate(build_execute_preflight_json(result))
@@ -752,7 +762,7 @@ def market_meta_morpho(
752
762
  vault_address: MetaMorpho V1 or Vault V2 address.
753
763
  chain_id: EVM chain ID.
754
764
  """
755
- info = fetch_vault(Web3.to_checksum_address(vault_address), chain_id)
765
+ info = fetch_vault(_address_arg(vault_address), chain_id)
756
766
  payload = build_meta_morpho_json(info, chain_id)
757
767
  return MetaMorphoVaultResponse.model_validate(payload)
758
768
 
@@ -236,15 +236,32 @@ def _resolve_middleware_fallback(
236
236
  node.source_type = TYPE_MIDDLEWARE_FALLBACK
237
237
  node.status = "resolved"
238
238
  if underlying_middleware is not None:
239
- # Same asset, different oracle — must not be in `visited` yet; runaway
240
- # manager→manager chains are bounded by max_depth instead.
241
- dep = _resolve(
242
- reader.delegate(underlying_middleware),
243
- asset,
244
- depth + 1,
245
- visited,
246
- max_depth,
247
- )
239
+ # Same asset, different oracle: the asset key is not in `visited` yet,
240
+ # so the hop is keyed by (oracle, asset) — a manager chain that loops
241
+ # back ends as a cycle instead of running to max_depth.
242
+ hop = f"{underlying_middleware.lower()}/{asset.lower()}"
243
+ if hop in visited:
244
+ return _partial(
245
+ asset,
246
+ symbol,
247
+ decimals,
248
+ source,
249
+ TYPE_UNKNOWN,
250
+ "cycle_detected",
251
+ [label],
252
+ price,
253
+ )
254
+ visited.add(hop)
255
+ try:
256
+ dep = _resolve(
257
+ reader.delegate(underlying_middleware),
258
+ asset,
259
+ depth + 1,
260
+ visited,
261
+ max_depth,
262
+ )
263
+ finally:
264
+ visited.discard(hop)
248
265
  node.dependencies = [dep]
249
266
  # dep re-resolves the same asset, so drop its leading label from the path.
250
267
  node.path = [label, "middleware fallback", *dep.path[1:]]
@@ -312,7 +312,21 @@ def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> Decimals |
312
312
  return decimals
313
313
 
314
314
 
315
- def _block_timestamp(ctx: Web3Context, block: int) -> Timestamp:
315
+ def _latest_head(ctx: Web3Context) -> tuple[int, Timestamp]:
316
+ """The latest block's number and timestamp from one read.
317
+
318
+ Reading the head number first and the block by number second can reach a
319
+ node behind a load balancer that has not seen that block yet.
320
+ """
321
+ head = ctx.web3.eth.get_block("latest")
322
+ return head["number"], head["timestamp"]
323
+
324
+
325
+ def _block_timestamp(
326
+ ctx: Web3Context, block: int, known: Timestamp | None
327
+ ) -> Timestamp:
328
+ if known is not None:
329
+ return known
316
330
  return ctx.web3.eth.get_block(block)["timestamp"]
317
331
 
318
332
 
@@ -634,14 +648,18 @@ def fetch_vault_data(
634
648
  ensure_supported_chain(ctx.chain_id)
635
649
  if block is None:
636
650
  block = _pinned_default_block(ctx)
637
- resolved_block = ctx.web3.eth.block_number if block is None else block
638
651
  is_latest = block is None
652
+ head_timestamp: Timestamp | None = None
653
+ if block is None:
654
+ resolved_block, head_timestamp = _latest_head(ctx)
655
+ else:
656
+ resolved_block = block
639
657
  ctx = ctx.at_block(resolved_block)
640
658
  plasma_vault = PlasmaVault(vault)
641
659
  with ThreadPoolExecutor() as pool:
642
660
  # Independent pipelines, each a short chain of batched reads; none of
643
661
  # them waits on a pool future, so they cannot starve the pool.
644
- f_timestamp = pool.submit(_block_timestamp, ctx, resolved_block)
662
+ f_timestamp = pool.submit(_block_timestamp, ctx, resolved_block, head_timestamp)
645
663
  f_vault = pool.submit(_fetch_vault_reads, ctx, plasma_vault)
646
664
  f_fees = pool.submit(_fetch_fee_data, ctx, plasma_vault)
647
665
  f_withdraw = pool.submit(_fetch_withdraw_manager, ctx, plasma_vault)
@@ -0,0 +1,17 @@
1
+ """Address spellings that EIP-55 must reject."""
2
+
3
+ from web3 import Web3
4
+
5
+
6
+ def miscased(address: str) -> str:
7
+ """``address`` checksummed with one letter's case flipped: the same 20 bytes,
8
+ a wrong checksum. Only a mixed-case result carries a checksum, so the first
9
+ flip that leaves the digits mixed case is taken."""
10
+ checksummed = Web3.to_checksum_address(address)
11
+ for i, c in enumerate(checksummed[2:], start=2):
12
+ if c.isalpha():
13
+ flipped = checksummed[:i] + c.swapcase() + checksummed[i + 1 :]
14
+ digits = flipped[2:]
15
+ if digits not in (digits.lower(), digits.upper()):
16
+ return flipped
17
+ raise AssertionError(f"no wrong checksum is possible for {address}")
@@ -0,0 +1,20 @@
1
+ """The ``miscased`` test helper returns a spelling EIP-55 rejects, or fails."""
2
+
3
+ import pytest
4
+ from _addresses import miscased
5
+
6
+ from ipor_fusion.types import parse_checksum_address
7
+
8
+
9
+ def test_skips_a_flip_that_leaves_one_case():
10
+ # Flipping B gives all lowercase, which carries no checksum.
11
+ address = "0xBca0000000000000000000000000000000000000"
12
+ bad = miscased(address)
13
+ assert bad.lower() == address.lower()
14
+ with pytest.raises(ValueError, match="invalid address checksum"):
15
+ parse_checksum_address(bad)
16
+
17
+
18
+ def test_fails_when_no_flip_leaves_mixed_case():
19
+ with pytest.raises(AssertionError, match="no wrong checksum"):
20
+ miscased("0xCc00000000000000000000000000000000000000")
@@ -443,7 +443,10 @@ class TestVaultInfo:
443
443
  mock_ctx.cache = NullCache()
444
444
  mock_ctx.web3.eth.block_number = 12345678
445
445
  mock_ctx.default_block = "latest"
446
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
446
+ mock_ctx.web3.eth.get_block.return_value = {
447
+ "number": 12345678,
448
+ "timestamp": 1700000000,
449
+ }
447
450
  mock_ctx.chain_id = 1
448
451
  mock_ctx.at_block.return_value = mock_ctx
449
452
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -573,7 +576,10 @@ class TestVaultInfo:
573
576
  mock_ctx.cache = NullCache()
574
577
  mock_ctx.web3.eth.block_number = 12345678
575
578
  mock_ctx.default_block = "latest"
576
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
579
+ mock_ctx.web3.eth.get_block.return_value = {
580
+ "number": 12345678,
581
+ "timestamp": 1700000000,
582
+ }
577
583
  mock_ctx.chain_id = 1
578
584
  mock_ctx.at_block.return_value = mock_ctx
579
585
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -654,7 +660,10 @@ class TestVaultInfo:
654
660
  mock_ctx.cache = NullCache()
655
661
  mock_ctx.web3.eth.block_number = 100
656
662
  mock_ctx.default_block = "latest"
657
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
663
+ mock_ctx.web3.eth.get_block.return_value = {
664
+ "number": 100,
665
+ "timestamp": 1700000000,
666
+ }
658
667
  mock_ctx.chain_id = 1
659
668
  mock_ctx.at_block.return_value = mock_ctx
660
669
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -723,7 +732,10 @@ class TestVaultInfo:
723
732
  mock_ctx.cache = NullCache()
724
733
  mock_ctx.web3.eth.block_number = 100
725
734
  mock_ctx.default_block = "latest"
726
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
735
+ mock_ctx.web3.eth.get_block.return_value = {
736
+ "number": 100,
737
+ "timestamp": 1700000000,
738
+ }
727
739
  mock_ctx.chain_id = 1
728
740
  mock_ctx.at_block.return_value = mock_ctx
729
741
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -804,7 +816,10 @@ class TestVaultInfo:
804
816
  mock_ctx.cache = NullCache()
805
817
  mock_ctx.web3.eth.block_number = 100
806
818
  mock_ctx.default_block = "latest"
807
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
819
+ mock_ctx.web3.eth.get_block.return_value = {
820
+ "number": 100,
821
+ "timestamp": 1700000000,
822
+ }
808
823
  mock_ctx.web3.eth.get_transaction.return_value = {"blockNumber": 18500000}
809
824
  mock_ctx.chain_id = 1
810
825
  mock_ctx.at_block.return_value = mock_ctx
@@ -875,7 +890,10 @@ class TestVaultInfo:
875
890
  mock_ctx.cache = NullCache()
876
891
  mock_ctx.web3.eth.block_number = 100
877
892
  mock_ctx.default_block = "latest"
878
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
893
+ mock_ctx.web3.eth.get_block.return_value = {
894
+ "number": 100,
895
+ "timestamp": 1700000000,
896
+ }
879
897
  mock_ctx.chain_id = 1
880
898
  mock_ctx.at_block.return_value = mock_ctx
881
899
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -986,7 +1004,10 @@ class TestVaultInfoJson:
986
1004
  mock_ctx.chain_id = 1
987
1005
  mock_ctx.web3.eth.block_number = 12345678
988
1006
  mock_ctx.default_block = "latest"
989
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
1007
+ mock_ctx.web3.eth.get_block.return_value = {
1008
+ "number": 12345678,
1009
+ "timestamp": 1700000000,
1010
+ }
990
1011
  mock_ctx.chain_id = 1
991
1012
  mock_ctx.at_block.return_value = mock_ctx
992
1013
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -1165,7 +1186,10 @@ class TestVaultInfoJson:
1165
1186
  mock_ctx.cache = NullCache()
1166
1187
  mock_ctx.web3.eth.block_number = 100
1167
1188
  mock_ctx.default_block = "latest"
1168
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
1189
+ mock_ctx.web3.eth.get_block.return_value = {
1190
+ "number": 100,
1191
+ "timestamp": 1700000000,
1192
+ }
1169
1193
  mock_ctx.web3.eth.get_transaction.return_value = {"blockNumber": 18500000}
1170
1194
  mock_ctx.chain_id = 1
1171
1195
  mock_ctx.at_block.return_value = mock_ctx
@@ -1246,7 +1270,10 @@ class TestVaultInfoJson:
1246
1270
  mock_ctx.cache = NullCache()
1247
1271
  mock_ctx.web3.eth.block_number = 100
1248
1272
  mock_ctx.default_block = "latest"
1249
- mock_ctx.web3.eth.get_block.return_value = {"timestamp": 1700000000}
1273
+ mock_ctx.web3.eth.get_block.return_value = {
1274
+ "number": 100,
1275
+ "timestamp": 1700000000,
1276
+ }
1250
1277
  mock_ctx.chain_id = 1
1251
1278
  mock_ctx.at_block.return_value = mock_ctx
1252
1279
  mock_ctx_cls.from_url.return_value = mock_ctx
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
6
6
 
7
7
  import click
8
8
  import pytest
9
+ from _addresses import miscased
9
10
  from _multicall import decode_aggregate3, multicall_aware
10
11
  from _validation import holding, validate
11
12
  from click.testing import CliRunner
@@ -131,9 +132,20 @@ class TestAddressType:
131
132
  checksum = Web3.to_checksum_address("0x" + "1a2b3c4d5e" * 4)
132
133
  assert ADDRESS.convert(checksum, None, None) == checksum
133
134
 
135
+ @pytest.mark.parametrize(
136
+ "raw",
137
+ [
138
+ "0X" + "ab" * 20,
139
+ "0X" + "AB" * 20,
140
+ "0X" + VALID_CHECKSUM[2:],
141
+ ],
142
+ )
143
+ def test_valid_with_uppercase_0x_prefix(self, raw: str):
144
+ assert _convert_via_cli(raw) == VALID_CHECKSUM
145
+
134
146
  def test_bad_checksum_mixed_case(self):
135
147
  good = str(Web3.to_checksum_address("0x" + "1a2b3c4d5e" * 4))
136
- bad = good[:-1] + good[-1].swapcase()
148
+ bad = miscased(good)
137
149
  runner = CliRunner()
138
150
 
139
151
  @click.command()
@@ -143,6 +155,8 @@ class TestAddressType:
143
155
 
144
156
  result = runner.invoke(cmd, [bad])
145
157
  assert result.exit_code != 0
158
+ assert "invalid address checksum" in result.output
159
+ assert good not in result.output
146
160
 
147
161
  def test_too_short(self):
148
162
  runner = CliRunner()
@@ -1158,17 +1172,6 @@ class TestPrintBalanceFusesTable:
1158
1172
  assert "Zero-Balance Fuses" not in out
1159
1173
 
1160
1174
 
1161
- class TestAddressTypeChecksumException:
1162
- @patch("ipor_fusion.cli.options.Web3.to_checksum_address")
1163
- def test_checksum_exception_caught(self, mock_checksum):
1164
- mock_checksum.side_effect = Exception("unexpected error")
1165
- mixed = "0xAbCdEf1234567890aBcDeF1234567890AbCdEf12"
1166
- with pytest.raises(
1167
- click.exceptions.BadParameter, match="invalid address checksum"
1168
- ):
1169
- ADDRESS.convert(mixed, None, None)
1170
-
1171
-
1172
1175
  class TestPrintReconciliation:
1173
1176
  def test_within_threshold(self, capsys):
1174
1177
  data = VaultData(
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
  from dataclasses import asdict
7
7
 
8
8
  import pytest
9
+ from _addresses import miscased
9
10
  from web3 import Web3
10
11
 
11
12
  from ipor_fusion.core.fusion_factory import FusionInstance
@@ -58,6 +59,8 @@ def test_from_dict_checksums_every_address_and_round_trips():
58
59
  (lambda d: d.update(extra=1), "extra"),
59
60
  (lambda d: d.update(plasma_vault="0x1234"), "address"),
60
61
  (lambda d: d.update(plasma_vault=" " + d["plasma_vault"]), "address"),
62
+ (lambda d: d.update(plasma_vault=miscased(d["plasma_vault"])), "checksum"),
63
+ (lambda d: d.update(price_manager=miscased(d["price_manager"])), "checksum"),
61
64
  (lambda d: d.update(asset_decimals="8"), "integer"),
62
65
  (lambda d: d.update(index=True), "integer"),
63
66
  (lambda d: d.update(asset_symbol=None), "string"),
@@ -4,6 +4,7 @@ import asyncio
4
4
  from unittest.mock import MagicMock, patch
5
5
 
6
6
  import pytest
7
+ from _addresses import miscased
7
8
  from mcp.server.fastmcp.exceptions import ToolError
8
9
  from web3 import Web3
9
10
 
@@ -26,6 +27,8 @@ from ipor_fusion.types import (
26
27
 
27
28
  VAULT = PlasmaVaultAddress(Web3.to_checksum_address("0x" + "11" * 20))
28
29
  SENDER = Web3.to_checksum_address("0x" + "33" * 20)
30
+ # Hex letters, so it can carry a wrong checksum; SENDER has none.
31
+ SENDER_HEX = "0x" + "ab" * 20
29
32
  FUSE = FuseAddress(Web3.to_checksum_address("0x" + "55" * 20))
30
33
  ACTION = AaveV3SupplyFuse(FUSE).supply(
31
34
  asset=Web3.to_checksum_address("0x" + "44" * 20), amount=Amount(10)
@@ -84,3 +87,9 @@ def test_bad_calldata_is_a_tool_error(preflight):
84
87
  with pytest.raises(ToolError, match="no actions"):
85
88
  _call(vault_address=VAULT, calldata=empty, sender=SENDER)
86
89
  preflight.assert_not_called()
90
+
91
+
92
+ def test_wrong_sender_checksum_is_a_tool_error(preflight):
93
+ with pytest.raises(ToolError, match="sender: invalid address checksum"):
94
+ _call(vault_address=VAULT, calldata=CALLDATA, sender=miscased(SENDER_HEX))
95
+ preflight.assert_not_called()
@@ -5,6 +5,7 @@ import importlib.metadata
5
5
  from unittest.mock import ANY, MagicMock, patch
6
6
 
7
7
  import pytest
8
+ from _addresses import miscased
8
9
  from mcp.server.fastmcp.exceptions import ToolError
9
10
  from web3 import Web3
10
11
 
@@ -770,7 +771,7 @@ _LOAN = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
770
771
  _COLLAT = "0xD2af830E8CBdFed6CC11Bab697bB25496ed6FA62"
771
772
  _ORACLE = "0x7C65985C35181D51eF7571FA40211B57659B7d80"
772
773
  _IRM = "0x870Ac11D48B15db9a138Cf899d20F13F79Ba00BC"
773
- _VAULT = "0xf9bDdD4A9b3a45F980E11Fdde96E16364DDBec49"
774
+ _VAULT = Web3.to_checksum_address("0xf9bddd4a9b3a45f980e11fdde96e16364ddbec49")
774
775
 
775
776
 
776
777
  def _stub_reader_instance():
@@ -1085,6 +1086,17 @@ class TestMarketMetaMorpho:
1085
1086
  with pytest.raises(MorphoApiError, match="not found"):
1086
1087
  market_meta_morpho(vault_address=_VAULT, chain_id=1)
1087
1088
 
1089
+ @patch("ipor_fusion.mcp.server.fetch_vault", return_value=_v1_info())
1090
+ def test_accepts_padded_bare_lowercase_address(self, fetch):
1091
+ market_meta_morpho(vault_address=f" {_VAULT[2:].lower()} ", chain_id=1)
1092
+ fetch.assert_called_once_with(_VAULT, 1)
1093
+
1094
+ @patch("ipor_fusion.mcp.server.fetch_vault", return_value=_v1_info())
1095
+ def test_rejects_a_wrong_checksum(self, fetch):
1096
+ with pytest.raises(ValueError, match="invalid address checksum"):
1097
+ market_meta_morpho(vault_address=miscased(_VAULT), chain_id=1)
1098
+ fetch.assert_not_called()
1099
+
1088
1100
 
1089
1101
  class TestVaultAddressArgument:
1090
1102
  """Tool callers may pad an address with whitespace or omit its ``0x``; the
@@ -1114,10 +1126,8 @@ class TestVaultAddressArgument:
1114
1126
  _vault_arg(raw)
1115
1127
 
1116
1128
  def test_rejects_a_wrong_checksum(self):
1117
- checksum = Web3.to_checksum_address(self.VAULT)
1118
- i = next(i for i, c in enumerate(checksum) if c in "abcdefABCDEF")
1119
- miscased = checksum[:i] + checksum[i].swapcase() + checksum[i + 1 :]
1120
- for raw in (miscased, "0X" + miscased[2:], miscased[2:]):
1129
+ bad = miscased(self.VAULT)
1130
+ for raw in (bad, "0X" + bad[2:], bad[2:]):
1121
1131
  with pytest.raises(ValueError, match="invalid address checksum"):
1122
1132
  _vault_arg(raw)
1123
1133
 
@@ -636,6 +636,26 @@ class TestResolve:
636
636
  "chainlink_feed_registry": None,
637
637
  }
638
638
 
639
+ def test_manager_delegating_to_itself_ends_as_a_cycle(self):
640
+ r = FakeReader()
641
+ weth = addr(1)
642
+ mw = addr(0x51)
643
+ zero = Web3.to_checksum_address(om.ZERO_ADDRESS)
644
+ r.symbols[weth] = "WETH"
645
+ r.decimals[weth] = 18
646
+ r.sources[weth] = zero
647
+ r.prices[weth] = Price(asset=weth, amount=2_000 * 10**18, decimals=18)
648
+ r.underlying_mw = mw
649
+ r.delegated = r # the manager names itself as its underlying middleware
650
+
651
+ node = om.resolve_asset(r, weth, max_depth=50)
652
+
653
+ # one hop back to the same manager, not 50 down to max_depth
654
+ (dep,) = node.dependencies
655
+ assert dep.reason == "cycle_detected"
656
+ assert not dep.dependencies
657
+ assert node.status == "partially_resolved"
658
+
639
659
  def test_zero_underlying_middleware_treated_as_absent(self):
640
660
  r = FakeReader()
641
661
  asset = addr(1)
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  import pytest
4
+ from _addresses import miscased
4
5
  from eth_abi import encode
5
6
  from hexbytes import HexBytes
6
7
  from web3 import Web3
@@ -161,10 +162,8 @@ class TestChecksumParsers:
161
162
  for bad in ("0x1234", " " + self.LOWER):
162
163
  with pytest.raises(ValueError):
163
164
  parser(bad)
164
- i = next(i for i, c in enumerate(checksum) if c in "abcdefABCDEF")
165
- miscased = checksum[:i] + checksum[i].swapcase() + checksum[i + 1 :]
166
165
  with pytest.raises(ValueError, match="invalid address checksum"):
167
- parser(miscased)
166
+ parser(miscased(checksum))
168
167
  # Two characters swapped: a different address that looks right.
169
168
  swapped = checksum[:2] + checksum[3] + checksum[2] + checksum[4:]
170
169
  assert swapped[2:4] != checksum[2:4]
@@ -5,7 +5,7 @@ import pytest
5
5
  from ipor_fusion import Web3Context
6
6
  from ipor_fusion.cache import NullCache
7
7
  from ipor_fusion.types import ChainId
8
- from ipor_fusion.vault_data import _pinned_default_block
8
+ from ipor_fusion.vault_data import _latest_head, _pinned_default_block
9
9
 
10
10
 
11
11
  def _ctx() -> tuple[Web3Context, MagicMock]:
@@ -32,3 +32,13 @@ def test_tag_or_hash_resolves_to_its_number_once(tag: str):
32
32
  ctx, web3 = _ctx()
33
33
  assert _pinned_default_block(ctx.at_block(tag)) == 4242 # type: ignore[arg-type]
34
34
  web3.eth.get_block.assert_called_once_with(tag)
35
+
36
+
37
+ def test_latest_head_reads_number_and_timestamp_in_one_call():
38
+ # The head number and then get_block(number) can reach a node behind a
39
+ # load balancer that has not seen that block yet.
40
+ web3 = MagicMock()
41
+ web3.eth.get_block.return_value = {"number": 4242, "timestamp": 1_700_000_000}
42
+ ctx = Web3Context(web3, ChainId(1), cache=NullCache())
43
+ assert _latest_head(ctx) == (4242, 1_700_000_000)
44
+ web3.eth.get_block.assert_called_once_with("latest")