ipor-fusion 4.1.0.dev50__tar.gz → 4.1.0.dev54__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.dev50 → ipor_fusion-4.1.0.dev54}/PKG-INFO +1 -1
  2. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/pyproject.toml +1 -1
  3. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/__init__.py +2 -0
  4. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/vault_health.py +6 -1
  5. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/mcp/models.py +10 -0
  6. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/mcp/server.py +3 -1
  7. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/__init__.py +2 -0
  8. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/oracle_mapping.py +4 -1
  9. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/oracle_mapping_nodes.py +41 -0
  10. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/oracle_mapping_reader.py +8 -0
  11. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/validation.py +7 -2
  12. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_erc20.py +11 -1
  13. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_oracle_mapping_fake.py +4 -0
  14. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/oracle_mapping_golden/mapping.json +16 -1
  15. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +16 -1
  16. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_oracle_mapping_golden.py +11 -0
  17. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_oracle_mapping.py +48 -0
  18. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_public_api.py +2 -0
  19. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_validation.py +33 -1
  20. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.claude-plugin/marketplace.json +0 -0
  21. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.claude-plugin/plugin.json +0 -0
  22. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.env.example +0 -0
  23. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.gemini/settings.json +0 -0
  24. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.git-blame-ignore-revs +0 -0
  25. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/cd.yml +0 -0
  26. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/ci.yml +0 -0
  27. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/commit-lint.yml +0 -0
  28. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/notify-slack.yml +0 -0
  29. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/python-build.yml +0 -0
  30. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/python-publish.yml +0 -0
  31. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/release.yml +0 -0
  32. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.github/workflows/report-slack.yml +0 -0
  33. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.gitignore +0 -0
  34. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.mcp.json +0 -0
  35. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.pre-commit-config.yaml +0 -0
  36. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/.python-version +0 -0
  37. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/AGENTS.md +0 -0
  38. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/CHANGELOG.md +0 -0
  39. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/CLAUDE.md +0 -0
  40. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/CODEOWNERS +0 -0
  41. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/LICENSE +0 -0
  42. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/MIGRATION_4.md +0 -0
  43. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/README.md +0 -0
  44. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/build.sh +0 -0
  45. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/CONTRIBUTING.md +0 -0
  46. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/README.md +0 -0
  47. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  48. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  49. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  50. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/external_state_margin_leg_base.py +0 -0
  51. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/hypercore_vault_official_deployment_hyperevm.py +0 -0
  52. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/examples/simple_aave_v3_supply_base.py +0 -0
  53. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/pytest.ini +0 -0
  54. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/scripts/commit-check.sh +0 -0
  55. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/scripts/fusion_error_signatures.py +0 -0
  56. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/skills/ipor-deploy-vault/SKILL.md +0 -0
  57. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/about.py +0 -0
  58. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cache.py +0 -0
  59. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/chains.py +0 -0
  60. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/__init__.py +0 -0
  61. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/address_cmd.py +0 -0
  62. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  63. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/config_cmd.py +0 -0
  64. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/config_store.py +0 -0
  65. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/main.py +0 -0
  66. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/market_cmd.py +0 -0
  67. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/options.py +0 -0
  68. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  69. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/preflight_cmd.py +0 -0
  70. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  71. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  72. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  73. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  74. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/config/__init__.py +0 -0
  75. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/config/roles.py +0 -0
  76. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/__init__.py +0 -0
  77. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/access.py +0 -0
  78. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/address.py +0 -0
  79. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/context.py +0 -0
  80. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/context_manager.py +0 -0
  81. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/contract.py +0 -0
  82. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/erc20.py +0 -0
  83. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/external_state_executor.py +0 -0
  84. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/fee_manager.py +0 -0
  85. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/fuse.py +0 -0
  86. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/fuse_action.py +0 -0
  87. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/fuse_decoding.py +0 -0
  88. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/fusion_factory.py +0 -0
  89. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/logs.py +0 -0
  90. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/multicall.py +0 -0
  91. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/oracle.py +0 -0
  92. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/plasma_vault.py +0 -0
  93. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/rewards_manager.py +0 -0
  94. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/simulation.py +0 -0
  95. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/simulation_result.py +0 -0
  96. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  97. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/__init__.py +0 -0
  98. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  99. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  100. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  101. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  102. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  103. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  104. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  105. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  106. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  107. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/contracts.py +0 -0
  108. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/discovery.py +0 -0
  109. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/errors.py +0 -0
  110. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  111. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  112. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  113. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  114. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/lane.py +0 -0
  115. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/logs.py +0 -0
  116. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/messages.py +0 -0
  117. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/simulation.py +0 -0
  118. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  119. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  120. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  121. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  122. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  123. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/crosschain/transport.py +0 -0
  124. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/dependency_graph.py +0 -0
  125. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/entry_cli.py +0 -0
  126. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/entry_mcp.py +0 -0
  127. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/errors.py +0 -0
  128. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/explorer.py +0 -0
  129. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/field_docs.py +0 -0
  130. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/__init__.py +0 -0
  131. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  132. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/async_action.py +0 -0
  133. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/base.py +0 -0
  134. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  135. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/erc4626.py +0 -0
  136. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  137. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/events.py +0 -0
  138. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/external_state.py +0 -0
  139. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
  140. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  141. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  142. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/hypercore.py +0 -0
  143. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
  144. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/hypercore_pre_hooks.py +0 -0
  145. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/merkl.py +0 -0
  146. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/morpho.py +0 -0
  147. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  148. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  149. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fuses/universal.py +0 -0
  150. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/fusion_errors.py +0 -0
  151. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/guide/__init__.py +0 -0
  152. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/guide/architecture.md +0 -0
  153. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/guide/glossary.md +0 -0
  154. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/guide/invariants.md +0 -0
  155. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/guide/quickstart.md +0 -0
  156. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/hypercore/__init__.py +0 -0
  157. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/market_ids.py +0 -0
  158. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/market_info.py +0 -0
  159. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/mcp/__init__.py +0 -0
  160. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/mcp/guide.py +0 -0
  161. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/morpho_api.py +0 -0
  162. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/preflight.py +0 -0
  163. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/py.typed +0 -0
  164. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/aave_v3.py +0 -0
  165. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/aave_v4.py +0 -0
  166. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/compound_v3.py +0 -0
  167. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/euler_v2.py +0 -0
  168. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/hypercore.py +0 -0
  169. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  170. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/lending_health.py +0 -0
  171. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/morpho.py +0 -0
  172. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/position_manager.py +0 -0
  173. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  174. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  175. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/readers/unpriceable.py +0 -0
  176. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/registry.py +0 -0
  177. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/substrates.py +0 -0
  178. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/types.py +0 -0
  179. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_data.py +0 -0
  180. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_info.py +0 -0
  181. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_managers.py +0 -0
  182. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_positions.py +0 -0
  183. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/src/ipor_fusion/vault_reads.py +0 -0
  184. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/UniswapV3UniversalRouter.py +0 -0
  185. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_addresses.py +0 -0
  186. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_crosschain.py +0 -0
  187. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_crosschain_lifecycle.py +0 -0
  188. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_crosschain_recovery.py +0 -0
  189. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_euler_v2.py +0 -0
  190. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_event_args.py +0 -0
  191. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_fake_rpc.py +0 -0
  192. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_foundry.py +0 -0
  193. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_golden.py +0 -0
  194. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_guide.py +0 -0
  195. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_hypercore_model.py +0 -0
  196. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_hypercore_shadow.py +0 -0
  197. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_live.py +0 -0
  198. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_multicall.py +0 -0
  199. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_simulate.py +0 -0
  200. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/_validation.py +0 -0
  201. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/addresses.py +0 -0
  202. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/conftest.py +0 -0
  203. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/constants.py +0 -0
  204. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  205. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/hypercore_market55.json +0 -0
  206. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/hypercore_official.json +0 -0
  207. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/meta_morpho_v1.json +0 -0
  208. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +0 -0
  209. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/meta_morpho_v2.json +0 -0
  210. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +0 -0
  211. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_api/market.json +0 -0
  212. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_api/vault_v1.json +0 -0
  213. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_api/vault_v2.json +0 -0
  214. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +0 -0
  215. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue.json +0 -0
  216. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue.mcp.json +0 -0
  217. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue_api_error.json +0 -0
  218. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +0 -0
  219. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue_no_api.json +0 -0
  220. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +0 -0
  221. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/simulate_v1_request.json +0 -0
  222. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/clean.json +0 -0
  223. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/clean.mcp.json +0 -0
  224. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  225. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/deployment_error.json +0 -0
  226. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/deployment_error.mcp.json +0 -0
  227. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/deployment_error.txt +0 -0
  228. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/fetched.cli.json +0 -0
  229. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/fetched.json +0 -0
  230. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/fetched.mcp.json +0 -0
  231. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/full.json +0 -0
  232. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/full.mcp.json +0 -0
  233. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/full.txt +0 -0
  234. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/rich.json +0 -0
  235. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/rich.mcp.json +0 -0
  236. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  237. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_about.py +0 -0
  238. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_access_manager.py +0 -0
  239. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_addresses_helper.py +0 -0
  240. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cache.py +0 -0
  241. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_chains.py +0 -0
  242. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_address.py +0 -0
  243. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_changelog.py +0 -0
  244. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_commands.py +0 -0
  245. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_config_store.py +0 -0
  246. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_helpers.py +0 -0
  247. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_hypercore.py +0 -0
  248. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_lending_health.py +0 -0
  249. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_market_golden.py +0 -0
  250. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_market_morpho_blue.py +0 -0
  251. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_mcp_parity.py +0 -0
  252. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_oracle_mapping.py +0 -0
  253. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_pending_requests_scan.py +0 -0
  254. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_preflight.py +0 -0
  255. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_role_accounts.py +0 -0
  256. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_vault_info_golden.py +0 -0
  257. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_vault_predates_getters.py +0 -0
  258. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_cli_vault_validate.py +0 -0
  259. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_contract_call.py +0 -0
  260. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_contract_encoder.py +0 -0
  261. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_ccip_events.py +0 -0
  262. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_ccip_indexer.py +0 -0
  263. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_ccip_quote.py +0 -0
  264. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_chainlink.py +0 -0
  265. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_contracts.py +0 -0
  266. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_encoding.py +0 -0
  267. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_errors.py +0 -0
  268. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_lane.py +0 -0
  269. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_layering.py +0 -0
  270. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_crosschain_transport.py +0 -0
  271. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_docs_typing.py +0 -0
  272. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_erc20.py +0 -0
  273. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_error_hierarchy.py +0 -0
  274. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_examples_crosschain.py +0 -0
  275. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  276. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_examples_hypercore_hyperevm.py +0 -0
  277. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_examples_vaults.py +0 -0
  278. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_explorer.py +0 -0
  279. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_external_state_events.py +0 -0
  280. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_external_state_executor.py +0 -0
  281. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_fee_manager.py +0 -0
  282. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_fuse_decoding.py +0 -0
  283. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_fuse_encoding.py +0 -0
  284. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_fusion_errors.py +0 -0
  285. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_fusion_instance.py +0 -0
  286. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_governance_wrappers.py +0 -0
  287. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_guide.py +0 -0
  288. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_deployment.py +0 -0
  289. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_events.py +0 -0
  290. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_flow.py +0 -0
  291. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_live.py +0 -0
  292. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_outcome.py +0 -0
  293. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_pre_hooks.py +0 -0
  294. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_readers.py +0 -0
  295. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_hypercore_simulation.py +0 -0
  296. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_logs.py +0 -0
  297. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_address.py +0 -0
  298. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_hypercore.py +0 -0
  299. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_models.py +0 -0
  300. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_preflight.py +0 -0
  301. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_server.py +0 -0
  302. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_mcp_vault_validate.py +0 -0
  303. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_multicall.py +0 -0
  304. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_oracle.py +0 -0
  305. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_plasma_vault.py +0 -0
  306. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_preflight.py +0 -0
  307. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_public_type_reachability.py +0 -0
  308. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_reader_encoding.py +0 -0
  309. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_registry.py +0 -0
  310. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_revert_decoding.py +0 -0
  311. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_rewards_manager.py +0 -0
  312. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_roles.py +0 -0
  313. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_sdk_layering.py +0 -0
  314. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  315. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  316. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  317. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  318. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  319. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  320. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  321. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  322. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  323. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_examples_arbitrum.py +0 -0
  324. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_examples_base.py +0 -0
  325. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_examples_crosschain.py +0 -0
  326. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_examples_hyperevm.py +0 -0
  327. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_external_state_executor_base.py +0 -0
  328. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  329. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  330. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  331. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_morpho_blue.py +0 -0
  332. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_pilot_v3.py +0 -0
  333. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  334. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  335. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  336. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  337. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_preflight_base.py +0 -0
  338. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_readers_integration.py +0 -0
  339. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_release_funds.py +0 -0
  340. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_swap_on_base.py +0 -0
  341. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_timelock_base.py +0 -0
  342. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  343. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulation_errors.py +0 -0
  344. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_simulation_observation.py +0 -0
  345. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_solidity_compiler.py +0 -0
  346. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_solidity_mirrors.py +0 -0
  347. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_substrates.py +0 -0
  348. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_types_and_events.py +0 -0
  349. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_unpriceable_tokens.py +0 -0
  350. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_data_block.py +0 -0
  351. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_dep_graph.py +0 -0
  352. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_info_fetched_golden.py +0 -0
  353. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_info_onchain_base.py +0 -0
  354. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  355. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_reconciliation_onchain_base.py +0 -0
  356. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_vault_simulator_blocks.py +0 -0
  357. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_web3_context.py +0 -0
  358. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/test_withdraw_manager.py +0 -0
  359. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/typed_client/consumer.py +0 -0
  360. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/tests/typed_client/rejected.py +0 -0
  361. {ipor_fusion-4.1.0.dev50 → ipor_fusion-4.1.0.dev54}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.1.0.dev50
3
+ Version: 4.1.0.dev54
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.dev50"
3
+ version = "4.1.0.dev54"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -325,6 +325,7 @@ from ipor_fusion.readers.oracle_mapping_nodes import (
325
325
  Erc4626DetailJson,
326
326
  MiddlewareFallbackDetailJson,
327
327
  MorphoFeedDetailJson,
328
+ MorphoOracleFeedJson,
328
329
  OracleAssetJson,
329
330
  OracleMappingJson,
330
331
  OracleNodeJson,
@@ -760,6 +761,7 @@ __all__ = [
760
761
  "Erc4626DetailJson",
761
762
  "MiddlewareFallbackDetailJson",
762
763
  "MorphoFeedDetailJson",
764
+ "MorphoOracleFeedJson",
763
765
  "OracleAssetJson",
764
766
  "OracleMappingJson",
765
767
  "OracleNodeJson",
@@ -12,6 +12,7 @@ from ipor_fusion.validation import (
12
12
  VaultValidation,
13
13
  )
14
14
  from ipor_fusion.vault_data import VaultData
15
+ from ipor_fusion.vault_erc20 import ERC20_CACHE_TOLERANCE
15
16
  from ipor_fusion.vault_info import health_check_lists
16
17
 
17
18
 
@@ -146,7 +147,11 @@ def _print_erc20_cache_divergence(
146
147
  f"ERC20 direct={direct_fmt} — likely stale cache",
147
148
  fg="yellow",
148
149
  )
149
- elif cached > 0 and direct > 0 and abs(cached - direct) / direct > 0.1:
150
+ elif (
151
+ cached > 0
152
+ and direct > 0
153
+ and abs(cached - direct) / direct > ERC20_CACHE_TOLERANCE
154
+ ):
150
155
  click.secho(
151
156
  f" ERC20_VAULT_BALANCE cached={cached_fmt}, "
152
157
  f"ERC20 direct={direct_fmt} — significant divergence",
@@ -564,11 +564,21 @@ class Erc4626Detail(_Base):
564
564
  rate: str | None
565
565
 
566
566
 
567
+ class MorphoOracleFeed(AggregatorFeedDetail):
568
+ """A feed behind a Morpho oracle; role names the getter it came from."""
569
+
570
+ role: str
571
+
572
+
567
573
  class MorphoFeedDetail(_Base):
568
574
  morpho_oracle: str
569
575
  collateral_token: str | None
570
576
  loan_token: str
571
577
  morpho_price: str | None
578
+ oracle_feeds: list[MorphoOracleFeed] | None = Field(
579
+ description="Feeds the oracle uses; null when it does not expose them, "
580
+ "so their freshness cannot be read."
581
+ )
572
582
 
573
583
 
574
584
  class MiddlewareFallbackDetail(_Base):
@@ -468,7 +468,9 @@ def vault_oracle_mapping(
468
468
  description, a foreign getter answering; still resolved, but verify the
469
469
  address yourself if identity matters), ERC4626PriceFeed (share→asset
470
470
  rate, recurses on the underlying), CollateralTokenOnMorphoMarketPriceFeed
471
- (recurses on the loan token), middleware_fallback (no per-vault source;
471
+ (recurses on the loan token; source_detail.oracle_feeds lists the feeds
472
+ behind the Morpho oracle, null when it does not expose them),
473
+ middleware_fallback (no per-vault source;
472
474
  priced by the oracle's underlying global middleware, followed as a
473
475
  dependency when discoverable), custom_unknown (partial). Classification is
474
476
  heuristic interface probing: it grades on-chain evidence and cannot prove
@@ -45,6 +45,7 @@ from ipor_fusion.readers.oracle_mapping_nodes import (
45
45
  Erc4626DetailJson,
46
46
  MiddlewareFallbackDetailJson,
47
47
  MorphoFeedDetailJson,
48
+ MorphoOracleFeedJson,
48
49
  OracleAssetJson,
49
50
  OracleMappingJson,
50
51
  OracleNodeJson,
@@ -94,6 +95,7 @@ __all__ = [
94
95
  "Erc4626DetailJson",
95
96
  "MiddlewareFallbackDetailJson",
96
97
  "MorphoFeedDetailJson",
98
+ "MorphoOracleFeedJson",
97
99
  "OracleAssetJson",
98
100
  "OracleMappingJson",
99
101
  "OracleNodeJson",
@@ -9,7 +9,8 @@ collateral feeds). Unknown feeds are reported as partial, never dropped.
9
9
  A zero source is not a dead end: the oracle may be a manager variant that
10
10
  delegates such assets to an underlying global middleware — that delegation is
11
11
  followed and reported as ``middleware_fallback``. Every aggregator-compatible
12
- read (Chainlink-tier leaves, dual-cross-reference component feeds) carries
12
+ read (Chainlink-tier leaves, dual-cross-reference component feeds, feeds
13
+ behind a Morpho oracle) carries
13
14
  ``description()`` and the full ``latestRoundData()`` round in
14
15
  ``source_detail`` — raw values plus ISO-8601 UTC twins of the timestamps,
15
16
  no staleness judgment; ``description`` is null when the feed does not
@@ -83,6 +84,7 @@ from ipor_fusion.readers.oracle_mapping_nodes import (
83
84
  _apply_dependency_status,
84
85
  _collect_unresolved,
85
86
  _mapping_status,
87
+ _morpho_oracle_feeds,
86
88
  _partial,
87
89
  _price_block,
88
90
  _resolve_chainlink,
@@ -181,6 +183,7 @@ def _resolve_morpho(
181
183
  "collateral_token": collateral,
182
184
  "loan_token": loan,
183
185
  "morpho_price": str(price) if price is not None else None,
186
+ "oracle_feeds": _morpho_oracle_feeds(reader, morpho_oracle),
184
187
  }
185
188
  dep = _resolve(reader, loan, depth + 1, visited, max_depth)
186
189
  node.dependencies = [dep]
@@ -30,6 +30,12 @@ TYPE_CHAINLINK = "ChainlinkAggregator"
30
30
  TYPE_CHAINLINK_STYLE = "chainlink_style"
31
31
  TYPE_ERC4626 = "ERC4626PriceFeed"
32
32
  TYPE_MORPHO = "CollateralTokenOnMorphoMarketPriceFeed"
33
+ MORPHO_ORACLE_FEED_ROLES = (
34
+ "BASE_FEED_1",
35
+ "BASE_FEED_2",
36
+ "QUOTE_FEED_1",
37
+ "QUOTE_FEED_2",
38
+ )
33
39
  TYPE_DUAL_XREF = "DualCrossReferencePriceFeed"
34
40
  TYPE_MIDDLEWARE_FALLBACK = "middleware_fallback"
35
41
  TYPE_UNKNOWN = "custom_unknown"
@@ -83,11 +89,21 @@ class Erc4626DetailJson(TypedDict):
83
89
  rate: str | None
84
90
 
85
91
 
92
+ class MorphoOracleFeedJson(AggregatorFeedDetailJson):
93
+ """A feed behind a Morpho oracle; ``role`` names the getter it came from."""
94
+
95
+ role: str
96
+
97
+
86
98
  class MorphoFeedDetailJson(TypedDict):
99
+ """``oracle_feeds`` lists the feeds the oracle uses; null when the oracle
100
+ does not expose them, so their freshness cannot be read."""
101
+
87
102
  morpho_oracle: ChecksumAddress
88
103
  collateral_token: ChecksumAddress | None
89
104
  loan_token: ChecksumAddress
90
105
  morpho_price: str | None
106
+ oracle_feeds: list[MorphoOracleFeedJson] | None
91
107
 
92
108
 
93
109
  class MiddlewareFallbackDetailJson(TypedDict):
@@ -413,6 +429,31 @@ def _resolve_chainlink(
413
429
  return node
414
430
 
415
431
 
432
+ def _morpho_oracle_feeds(
433
+ reader: OracleMappingReader, morpho_oracle: ChecksumAddress
434
+ ) -> list[MorphoOracleFeedJson] | None:
435
+ """The feeds behind a Morpho oracle, skipping its unused (zero) slots."""
436
+ feeds: list[MorphoOracleFeedJson] = []
437
+ for role in MORPHO_ORACLE_FEED_ROLES:
438
+ address = reader.morpho_oracle_feed(morpho_oracle, role)
439
+ if address is None:
440
+ return None
441
+ if address.lower() == ZERO_ADDRESS:
442
+ continue
443
+ feeds.append(
444
+ {
445
+ "address": address,
446
+ **_aggregator_detail(
447
+ reader.feed_description(address),
448
+ reader.feed_latest_round_data(address),
449
+ reader.feed_decimals(address),
450
+ ),
451
+ "role": role,
452
+ }
453
+ )
454
+ return feeds
455
+
456
+
416
457
  def _resolve_dual_xref(
417
458
  reader: OracleMappingReader,
418
459
  node: OracleNode,
@@ -59,6 +59,9 @@ class _MorphoOracle(ContractWrapper):
59
59
  def price(self) -> Call[int]:
60
60
  return self._view("price()", output_types=["uint256"], decoder=int)
61
61
 
62
+ def feed(self, role: str) -> Call[ChecksumAddress]:
63
+ return self._view(f"{role}()", output_types=["address"], decoder=_addr)
64
+
62
65
 
63
66
  class _DualXrefFeed(ContractWrapper):
64
67
  def asset_x(self) -> Call[ChecksumAddress]:
@@ -222,6 +225,11 @@ class OracleMappingReader:
222
225
  def morpho_oracle_price(self, morpho_oracle: ChecksumAddress) -> int | None:
223
226
  return self._safe(_MorphoOracle(morpho_oracle).price())
224
227
 
228
+ def morpho_oracle_feed(
229
+ self, morpho_oracle: ChecksumAddress, role: str
230
+ ) -> ChecksumAddress | None:
231
+ return self._safe(_MorphoOracle(morpho_oracle).feed(role))
232
+
225
233
  def feed_asset_x(self, source: ChecksumAddress) -> ChecksumAddress | None:
226
234
  return self._safe(_DualXrefFeed(source).asset_x())
227
235
 
@@ -43,6 +43,10 @@ from ipor_fusion.vault_data import (
43
43
  GET_TOTAL_SUPPLY_CAP,
44
44
  fetch_vault_data,
45
45
  )
46
+ from ipor_fusion.vault_erc20 import (
47
+ ERC20_CACHE_TOLERANCE,
48
+ cache_lags_value,
49
+ )
46
50
  from ipor_fusion.vault_erc20 import Erc20Holding as Erc20Holding
47
51
  from ipor_fusion.vault_erc20 import Erc20Holdings as Erc20Holdings
48
52
 
@@ -704,7 +708,8 @@ def _erc20_findings(state: VaultState, erc20: Erc20Holdings) -> list[Finding]:
704
708
  non_underlying = erc20.value_in_asset - underlying
705
709
  cached = erc20.market_cached_value
706
710
  if non_underlying > 0 and (
707
- cached == 0 or abs(non_underlying - cached) / non_underlying > 0.1
711
+ cached == 0
712
+ or abs(non_underlying - cached) / non_underlying > ERC20_CACHE_TOLERANCE
708
713
  ):
709
714
  findings.append(
710
715
  Finding(
@@ -747,7 +752,7 @@ def _uncovered_erc20_lines(state: VaultState, erc20: Erc20Holdings) -> list[str]
747
752
  )
748
753
  if address not in covered:
749
754
  lines.append(f"{row} — not in any balance fuse substrate")
750
- elif 0 < cached_usd < (token.usd_value or 0):
755
+ elif cached_usd > 0 and cache_lags_value(token.usd_value or 0, cached_usd):
751
756
  lines.append(
752
757
  f"{row} — {usd.strip()} not reflected in totalAssets (stale cache)"
753
758
  )
@@ -19,6 +19,16 @@ from ipor_fusion.market_ids import IporFusionMarkets
19
19
  from ipor_fusion.substrates import decode_substrate
20
20
  from ipor_fusion.types import Amount, Decimals, MarketId, PlasmaVaultAddress, Price
21
21
 
22
+ # The cached value is written at an earlier price and the live one is rounded,
23
+ # so the two never match exactly; a gap within this share of the live value is
24
+ # not staleness.
25
+ ERC20_CACHE_TOLERANCE = 0.1
26
+
27
+
28
+ def cache_lags_value(usd_value: float, cached_usd: float) -> bool:
29
+ """Whether a live USD value exceeds the market's cached value beyond tolerance."""
30
+ return usd_value - cached_usd > ERC20_CACHE_TOLERANCE * usd_value
31
+
22
32
 
23
33
  @dataclass(frozen=True)
24
34
  class Erc20Holding:
@@ -216,7 +226,7 @@ def _holding(
216
226
  elif (
217
227
  read.price_usd
218
228
  and cached_usd is not None
219
- and amount * read.price_usd > cached_usd
229
+ and cache_lags_value(amount * read.price_usd, cached_usd)
220
230
  ):
221
231
  note = (
222
232
  f"${amount * read.price_usd:,.2f} not reflected in totalAssets "
@@ -52,6 +52,7 @@ class FakeReader:
52
52
  self.feeds: dict[str, Feed] = {}
53
53
  self.vaults: dict[str, Erc4626] = {}
54
54
  self.morpho_prices: dict[str, int] = {}
55
+ self.morpho_feeds: dict[str, dict[str, str]] = {}
55
56
  # oracle variant (middleware-fallback resolution); "_mw" because the
56
57
  # exact method name would be shadowed by the attribute
57
58
  self.underlying_mw: str | None = None
@@ -125,6 +126,9 @@ class FakeReader:
125
126
  def morpho_oracle_price(self, o: str) -> int | None:
126
127
  return self.morpho_prices.get(o)
127
128
 
129
+ def morpho_oracle_feed(self, o: str, role: str) -> str | None:
130
+ return self.morpho_feeds.get(o, {}).get(role)
131
+
128
132
  def feed_asset_x(self, s: str) -> str | None:
129
133
  return self._f(s).get("asset_x")
130
134
 
@@ -300,7 +300,22 @@
300
300
  "morpho_oracle": "0x0000000000000000000000000000000000000031",
301
301
  "collateral_token": "0x0000000000000000000000000000000000000006",
302
302
  "loan_token": "0x0000000000000000000000000000000000000001",
303
- "morpho_price": "970000000000000000000000"
303
+ "morpho_price": "970000000000000000000000",
304
+ "oracle_feeds": [
305
+ {
306
+ "address": "0x0000000000000000000000000000000000000032",
307
+ "description": "COLL / LOAN",
308
+ "round_id": "9",
309
+ "answer": "970000000000000000",
310
+ "decimals": 18,
311
+ "started_at": 1700000000,
312
+ "started_at_utc": "2023-11-14T22:13:20Z",
313
+ "updated_at": 1700000050,
314
+ "updated_at_utc": "2023-11-14T22:14:10Z",
315
+ "answered_in_round": "9",
316
+ "role": "BASE_FEED_1"
317
+ }
318
+ ]
304
319
  },
305
320
  "dependencies": [
306
321
  {
@@ -300,7 +300,22 @@
300
300
  "morpho_oracle": "0x0000000000000000000000000000000000000031",
301
301
  "collateral_token": "0x0000000000000000000000000000000000000006",
302
302
  "loan_token": "0x0000000000000000000000000000000000000001",
303
- "morpho_price": "970000000000000000000000"
303
+ "morpho_price": "970000000000000000000000",
304
+ "oracle_feeds": [
305
+ {
306
+ "address": "0x0000000000000000000000000000000000000032",
307
+ "description": "COLL / LOAN",
308
+ "round_id": "9",
309
+ "answer": "970000000000000000",
310
+ "decimals": 18,
311
+ "started_at": 1700000000,
312
+ "started_at_utc": "2023-11-14T22:13:20Z",
313
+ "updated_at": 1700000050,
314
+ "updated_at_utc": "2023-11-14T22:14:10Z",
315
+ "answered_in_round": "9",
316
+ "role": "BASE_FEED_1"
317
+ }
318
+ ]
304
319
  },
305
320
  "dependencies": [
306
321
  {
@@ -154,6 +154,17 @@ def _reader() -> FakeReader:
154
154
  "loan": USDC,
155
155
  }
156
156
  r.morpho_prices[addr(0x31)] = 970_000 * 10**18
157
+ r.morpho_feeds[addr(0x31)] = {
158
+ "BASE_FEED_1": addr(0x32),
159
+ "BASE_FEED_2": addr(0),
160
+ "QUOTE_FEED_1": addr(0),
161
+ "QUOTE_FEED_2": addr(0),
162
+ }
163
+ r.feeds[addr(0x32)] = {
164
+ "round": (9, 97 * 10**16, 1_700_000_000, 1_700_000_050, 9),
165
+ "feed_decimals": 18,
166
+ "description": "COLL / LOAN",
167
+ }
157
168
 
158
169
  r.symbols[WAT] = "WAT"
159
170
  r.decimals[WAT] = 18
@@ -270,6 +270,53 @@ class TestResolve:
270
270
  assert [d.asset for d in node.dependencies] == [loan]
271
271
  assert node.path[:2] == ["COLL", "Morpho collateral/loan oracle"]
272
272
 
273
+ def test_morpho_oracle_feeds_report_their_rounds_and_skip_unused_slots(self):
274
+ r = FakeReader()
275
+ coll, loan = addr(1), addr(2)
276
+ feed, m_oracle, base_feed = addr(0x11), addr(0x31), addr(0x41)
277
+ r.sources[coll] = feed
278
+ r.prices[coll] = Price(asset=coll, amount=10**8, decimals=8)
279
+ r.feeds[feed] = {"morpho_oracle": m_oracle, "collateral": coll, "loan": loan}
280
+ r.feeds[base_feed] = {
281
+ "round": (7, 10**18, 1_700_000_000, 1_700_000_100, 7),
282
+ "feed_decimals": 18,
283
+ "description": "COLL / LOAN",
284
+ }
285
+ zero = addr(0)
286
+ r.morpho_feeds[m_oracle] = {
287
+ "BASE_FEED_1": base_feed,
288
+ "BASE_FEED_2": zero,
289
+ "QUOTE_FEED_1": zero,
290
+ "QUOTE_FEED_2": zero,
291
+ }
292
+ chainlink_asset(r, loan, addr(0x12), symbol="LOAN")
293
+
294
+ detail = om.resolve_asset(r, coll, max_depth=6).source_detail
295
+
296
+ assert detail is not None and "oracle_feeds" in detail
297
+ feeds = detail["oracle_feeds"]
298
+ assert feeds is not None
299
+ assert [(f["role"], f["address"]) for f in feeds] == [
300
+ ("BASE_FEED_1", base_feed)
301
+ ]
302
+ assert feeds[0]["description"] == "COLL / LOAN"
303
+ assert feeds[0]["updated_at"] == 1_700_000_100
304
+
305
+ def test_morpho_oracle_without_the_feed_getters_reports_none(self):
306
+ r = FakeReader()
307
+ coll, loan = addr(1), addr(2)
308
+ feed, m_oracle = addr(0x11), addr(0x31)
309
+ r.sources[coll] = feed
310
+ r.prices[coll] = Price(asset=coll, amount=10**8, decimals=8)
311
+ r.feeds[feed] = {"morpho_oracle": m_oracle, "collateral": coll, "loan": loan}
312
+ chainlink_asset(r, loan, addr(0x12), symbol="LOAN")
313
+
314
+ node = om.resolve_asset(r, coll, max_depth=6)
315
+
316
+ assert node.status == "resolved"
317
+ assert node.source_detail is not None and "oracle_feeds" in node.source_detail
318
+ assert node.source_detail["oracle_feeds"] is None
319
+
273
320
  def test_dual_xref_wins_over_chainlink(self):
274
321
  r = FakeReader()
275
322
  wsteth = addr(1)
@@ -1165,6 +1212,7 @@ class TestOracleMappingReader:
1165
1212
  assert reader.feed_asset_x_asset_y_feed(feed) == target
1166
1213
  assert reader.feed_asset_y_usd_feed(feed) == target
1167
1214
  assert reader.feed_aggregator(feed) == target
1215
+ assert reader.morpho_oracle_feed(addr(0x31), "BASE_FEED_1") == target
1168
1216
 
1169
1217
  ctx.call.return_value = encode(["uint256"], [1_030_000])
1170
1218
  assert reader.morpho_oracle_price(addr(0x31)) == 1_030_000
@@ -228,6 +228,7 @@ _ALL_SNAPSHOT: dict[str, frozenset[str]] = {
228
228
  "MorphoClaimFuse",
229
229
  "MorphoCollateralFuse",
230
230
  "MorphoFeedDetailJson",
231
+ "MorphoOracleFeedJson",
231
232
  "MorphoFlashLoanFuse",
232
233
  "MorphoFlowCapConfigJson",
233
234
  "MorphoMarket",
@@ -952,6 +953,7 @@ _ALL_SNAPSHOT: dict[str, frozenset[str]] = {
952
953
  "MiddlewarePricedToken",
953
954
  "UnpriceableCheck",
954
955
  "MorphoFeedDetailJson",
956
+ "MorphoOracleFeedJson",
955
957
  "MorphoMarket",
956
958
  "MorphoMarketParams",
957
959
  "MorphoMarketRates",
@@ -40,7 +40,13 @@ from ipor_fusion.market_ids import IporFusionMarkets
40
40
  from ipor_fusion.types import ChainId, MarketId
41
41
  from ipor_fusion.validation import GET_MARKET_SUBSTRATES, Reconciliation
42
42
  from ipor_fusion.vault_data import VaultData, _MarketReads, _VaultReads
43
- from ipor_fusion.vault_erc20 import _Asset, _erc20_holdings, _read_erc20_market
43
+ from ipor_fusion.vault_erc20 import (
44
+ _Asset,
45
+ _erc20_holdings,
46
+ _Erc20Market,
47
+ _read_erc20_market,
48
+ _TokenRead,
49
+ )
44
50
 
45
51
 
46
52
  def _addr(suffix: str) -> str:
@@ -57,6 +63,11 @@ ORPHAN_FUSE = _addr("f5")
57
63
  AAVE = IporFusionMarkets.AAVE_V3
58
64
  MORPHO = IporFusionMarkets.MORPHO
59
65
  ERC20_MARKET = IporFusionMarkets.ERC20_VAULT_BALANCE
66
+ # A token whose value matches the market's cached value but for float rounding.
67
+ ASSET_PRICE = 2594.826591677957
68
+ ROUNDED_BALANCE = 3663278943628990
69
+ ROUNDED_PRICE = 2269.8825
70
+ ROUNDED_CACHE = 3204535051949216
60
71
  WAD = 10**18
61
72
 
62
73
 
@@ -276,6 +287,16 @@ class TestRouting:
276
287
  assert heading == "ERC20 holdings not covered by balance fuses:"
277
288
  assert [row.split()[0] for row in rows] == ["AAA", "BBB"]
278
289
 
290
+ def test_rounding_gap_to_the_cache_is_not_uncovered(self):
291
+ state = _erc20_state(asset_price_usd=ASSET_PRICE)
292
+ erc20 = Erc20Holdings(
293
+ tokens=(holding(TOKEN, balance=ROUNDED_BALANCE, price_usd=ROUNDED_PRICE),),
294
+ value_in_asset=ROUNDED_CACHE,
295
+ market_cached_value=ROUNDED_CACHE,
296
+ )
297
+ validation = validate(state, 100 * WAD, erc20)
298
+ assert "uncovered_erc20" not in [check for check, _ in _checks(validation)]
299
+
279
300
  def test_stale_erc20_cache_and_missing_price_feed(self):
280
301
  erc20 = Erc20Holdings(
281
302
  value_in_asset=50 * WAD,
@@ -598,6 +619,17 @@ class TestReadErc20Holdings:
598
619
  assert erc20.market_cached_value == 2 * WAD
599
620
  assert erc20.unpriced_tokens == (f"{OTHER.lower()} (?)",)
600
621
 
622
+ def test_rounding_gap_to_the_cache_keeps_the_token_in_the_market(self):
623
+ erc20 = _erc20_holdings(
624
+ _Asset(ASSET, 18, ASSET_PRICE), # type: ignore[arg-type]
625
+ _Erc20Market( # type: ignore[arg-type]
626
+ ROUNDED_CACHE,
627
+ (_TokenRead(TOKEN, 18, ROUNDED_BALANCE, ROUNDED_PRICE),), # type: ignore[arg-type]
628
+ ),
629
+ {},
630
+ )
631
+ assert erc20.tokens[0].note == "in ERC20_VAULT_BALANCE"
632
+
601
633
  def test_symbols_name_the_holdings(self):
602
634
  symbols = {a.lower(): a.lower()[-2:] for a in (ASSET, TOKEN, OTHER)}
603
635
  erc20 = self._read(_erc20_state(), symbols)