ipor-fusion 4.0.0.dev164__tar.gz → 4.0.0.dev171__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 (332) hide show
  1. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/AGENTS.md +10 -0
  2. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/MIGRATION_4.md +26 -1
  3. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/PKG-INFO +1 -1
  4. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/advanced_euler_v2_credit_market_base.py +1 -3
  5. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/advanced_euler_v2_looping_arbitrum.py +1 -3
  6. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +1 -1
  7. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +1 -1
  8. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/external_state_margin_leg_base.py +1 -3
  9. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/simple_aave_v3_supply_base.py +1 -3
  10. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/pyproject.toml +1 -1
  11. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/__init__.py +2 -2
  12. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/simulation.py +52 -19
  13. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/__init__.py +1 -1
  14. ipor_fusion-4.0.0.dev171/tests/_golden.py +69 -0
  15. ipor_fusion-4.0.0.dev171/tests/_oracle_mapping_fake.py +200 -0
  16. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/meta_morpho_v1.json +99 -0
  17. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +111 -0
  18. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/meta_morpho_v2.json +135 -0
  19. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +140 -0
  20. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_api/market.json +208 -0
  21. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_api/vault_v1.json +168 -0
  22. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_api/vault_v2.json +115 -0
  23. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +12 -0
  24. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue.json +107 -0
  25. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue.mcp.json +108 -0
  26. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue_api_error.json +28 -0
  27. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +31 -0
  28. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue_no_api.json +27 -0
  29. ipor_fusion-4.0.0.dev171/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +31 -0
  30. ipor_fusion-4.0.0.dev171/tests/fixtures/oracle_mapping_golden/mapping.json +573 -0
  31. ipor_fusion-4.0.0.dev171/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +573 -0
  32. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/clean.mcp.json +136 -0
  33. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/deployment_error.json +131 -0
  34. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/deployment_error.mcp.json +138 -0
  35. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/deployment_error.txt +54 -0
  36. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/full.json +688 -0
  37. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/full.mcp.json +709 -0
  38. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/full.txt +154 -0
  39. ipor_fusion-4.0.0.dev171/tests/fixtures/vault_info_golden/rich.mcp.json +406 -0
  40. ipor_fusion-4.0.0.dev171/tests/test_cli_market_golden.py +199 -0
  41. ipor_fusion-4.0.0.dev171/tests/test_cli_oracle_mapping_golden.py +275 -0
  42. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_vault_info_golden.py +264 -16
  43. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_oracle_mapping.py +25 -193
  44. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_public_api.py +10 -0
  45. ipor_fusion-4.0.0.dev171/tests/test_simulation_errors.py +229 -0
  46. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.claude-plugin/marketplace.json +0 -0
  47. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.claude-plugin/plugin.json +0 -0
  48. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.env.example +0 -0
  49. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.gemini/settings.json +0 -0
  50. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.git-blame-ignore-revs +0 -0
  51. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/cd.yml +0 -0
  52. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/ci.yml +0 -0
  53. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/commit-lint.yml +0 -0
  54. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/notify-slack.yml +0 -0
  55. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/python-build.yml +0 -0
  56. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/python-publish.yml +0 -0
  57. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/release.yml +0 -0
  58. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.github/workflows/report-slack.yml +0 -0
  59. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.gitignore +0 -0
  60. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.mcp.json +0 -0
  61. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.pre-commit-config.yaml +0 -0
  62. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/.python-version +0 -0
  63. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/CHANGELOG.md +0 -0
  64. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/CLAUDE.md +0 -0
  65. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/CODEOWNERS +0 -0
  66. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/LICENSE +0 -0
  67. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/README.md +0 -0
  68. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/build.sh +0 -0
  69. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/CONTRIBUTING.md +0 -0
  70. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/examples/README.md +0 -0
  71. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/pytest.ini +0 -0
  72. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/scripts/commit-check.sh +0 -0
  73. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/scripts/crosschain_readiness.py +0 -0
  74. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/skills/ipor-deploy-vault/SKILL.md +0 -0
  75. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/about.py +0 -0
  76. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cache.py +0 -0
  77. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/chains.py +0 -0
  78. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/__init__.py +0 -0
  79. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  80. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/config_cmd.py +0 -0
  81. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/config_store.py +0 -0
  82. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/main.py +0 -0
  83. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/market_cmd.py +0 -0
  84. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/options.py +0 -0
  85. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  86. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  87. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/vault_health.py +0 -0
  88. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  89. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  90. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  91. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/config/__init__.py +0 -0
  92. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/config/roles.py +0 -0
  93. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/__init__.py +0 -0
  94. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/access.py +0 -0
  95. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/address.py +0 -0
  96. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/context.py +0 -0
  97. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/contract.py +0 -0
  98. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/erc20.py +0 -0
  99. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/external_state_executor.py +0 -0
  100. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/fee_manager.py +0 -0
  101. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/fuse.py +0 -0
  102. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/fuse_action.py +0 -0
  103. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/fusion_factory.py +0 -0
  104. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/logs.py +0 -0
  105. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/multicall.py +0 -0
  106. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/oracle.py +0 -0
  107. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/plasma_vault.py +0 -0
  108. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/rewards_manager.py +0 -0
  109. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  110. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/__init__.py +0 -0
  111. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  112. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  113. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  114. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  115. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  116. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  117. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  118. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  119. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  120. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/contracts.py +0 -0
  121. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/discovery.py +0 -0
  122. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/errors.py +0 -0
  123. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  124. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  125. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  126. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  127. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/lane.py +0 -0
  128. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/logs.py +0 -0
  129. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/messages.py +0 -0
  130. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/simulation.py +0 -0
  131. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  132. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  133. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  134. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  135. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  136. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/crosschain/transport.py +0 -0
  137. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/dependency_graph.py +0 -0
  138. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/entry_cli.py +0 -0
  139. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/entry_mcp.py +0 -0
  140. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/errors.py +0 -0
  141. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/explorer.py +0 -0
  142. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/field_docs.py +0 -0
  143. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  144. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/async_action.py +0 -0
  145. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/base.py +0 -0
  146. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  147. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/erc4626.py +0 -0
  148. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  149. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/events.py +0 -0
  150. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/external_state.py +0 -0
  151. /ipor_fusion-4.0.0.dev164/src/ipor_fusion/fuses/external_state_events.py → /ipor_fusion-4.0.0.dev171/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
  152. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  153. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  154. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/hypercore.py +0 -0
  155. /ipor_fusion-4.0.0.dev164/src/ipor_fusion/fuses/hypercore_events.py → /ipor_fusion-4.0.0.dev171/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
  156. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/merkl.py +0 -0
  157. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/morpho.py +0 -0
  158. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  159. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  160. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/fuses/universal.py +0 -0
  161. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/guide/__init__.py +0 -0
  162. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/guide/architecture.md +0 -0
  163. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/guide/glossary.md +0 -0
  164. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/guide/invariants.md +0 -0
  165. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/guide/quickstart.md +0 -0
  166. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/market_ids.py +0 -0
  167. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/market_info.py +0 -0
  168. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/mcp/__init__.py +0 -0
  169. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/mcp/guide.py +0 -0
  170. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/mcp/models.py +0 -0
  171. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/mcp/server.py +0 -0
  172. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/morpho_api.py +0 -0
  173. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/py.typed +0 -0
  174. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/__init__.py +0 -0
  175. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/aave_v3.py +0 -0
  176. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/aave_v4.py +0 -0
  177. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/compound_v3.py +0 -0
  178. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/hypercore.py +0 -0
  179. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  180. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/lending_health.py +0 -0
  181. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/morpho.py +0 -0
  182. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  183. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  184. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  185. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/position_manager.py +0 -0
  186. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  187. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  188. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/readers/unpriceable.py +0 -0
  189. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/substrates.py +0 -0
  190. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/types.py +0 -0
  191. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/validation.py +0 -0
  192. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_data.py +0 -0
  193. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_erc20.py +0 -0
  194. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_info.py +0 -0
  195. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_managers.py +0 -0
  196. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_positions.py +0 -0
  197. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/src/ipor_fusion/vault_reads.py +0 -0
  198. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/UniswapV3UniversalRouter.py +0 -0
  199. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_crosschain.py +0 -0
  200. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_crosschain_lifecycle.py +0 -0
  201. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_crosschain_recovery.py +0 -0
  202. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_euler_v2.py +0 -0
  203. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_event_args.py +0 -0
  204. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_foundry.py +0 -0
  205. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_guide.py +0 -0
  206. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_hypercore_model.py +0 -0
  207. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_hypercore_shadow.py +0 -0
  208. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_multicall.py +0 -0
  209. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_simulate.py +0 -0
  210. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/_validation.py +0 -0
  211. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/addresses.py +0 -0
  212. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/conftest.py +0 -0
  213. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/constants.py +0 -0
  214. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  215. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/hypercore_market55.json +0 -0
  216. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/simulate_v1_request.json +0 -0
  217. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/vault_info_golden/clean.json +0 -0
  218. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  219. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/vault_info_golden/rich.json +0 -0
  220. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  221. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_about.py +0 -0
  222. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_access_manager.py +0 -0
  223. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cache.py +0 -0
  224. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_chains.py +0 -0
  225. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_changelog.py +0 -0
  226. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_commands.py +0 -0
  227. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_config_store.py +0 -0
  228. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_helpers.py +0 -0
  229. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_hypercore.py +0 -0
  230. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_lending_health.py +0 -0
  231. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_market_morpho_blue.py +0 -0
  232. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_oracle_mapping.py +0 -0
  233. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_pending_requests_scan.py +0 -0
  234. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_role_accounts.py +0 -0
  235. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_vault_predates_getters.py +0 -0
  236. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_cli_vault_validate.py +0 -0
  237. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_contract_call.py +0 -0
  238. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_contract_encoder.py +0 -0
  239. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_ccip_events.py +0 -0
  240. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_ccip_indexer.py +0 -0
  241. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_ccip_quote.py +0 -0
  242. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_chainlink.py +0 -0
  243. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_contracts.py +0 -0
  244. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_encoding.py +0 -0
  245. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_errors.py +0 -0
  246. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_lane.py +0 -0
  247. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_layering.py +0 -0
  248. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_readiness.py +0 -0
  249. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_readiness_report.py +0 -0
  250. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_crosschain_transport.py +0 -0
  251. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_docs_typing.py +0 -0
  252. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_erc20.py +0 -0
  253. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_error_hierarchy.py +0 -0
  254. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_examples_composed_hypercore.py +0 -0
  255. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_examples_crosschain.py +0 -0
  256. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  257. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_examples_vaults.py +0 -0
  258. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_explorer.py +0 -0
  259. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_external_state_events.py +0 -0
  260. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_external_state_executor.py +0 -0
  261. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_fee_manager.py +0 -0
  262. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_fuse_encoding.py +0 -0
  263. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_fusion_instance.py +0 -0
  264. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_guide.py +0 -0
  265. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_deployment.py +0 -0
  266. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_events.py +0 -0
  267. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_flow.py +0 -0
  268. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_live.py +0 -0
  269. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_outcome.py +0 -0
  270. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_readers.py +0 -0
  271. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_hypercore_simulation.py +0 -0
  272. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_logs.py +0 -0
  273. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_mcp_hypercore.py +0 -0
  274. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_mcp_models.py +0 -0
  275. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_mcp_server.py +0 -0
  276. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_mcp_vault_validate.py +0 -0
  277. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_multicall.py +0 -0
  278. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_oracle.py +0 -0
  279. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_plasma_vault.py +0 -0
  280. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_public_type_reachability.py +0 -0
  281. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_reader_encoding.py +0 -0
  282. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_readiness_selection.py +0 -0
  283. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_revert_decoding.py +0 -0
  284. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_rewards_manager.py +0 -0
  285. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_roles.py +0 -0
  286. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_sdk_layering.py +0 -0
  287. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  288. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  289. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  290. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_crosschain_canary.py +0 -0
  291. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_crosschain_hypercore.py +0 -0
  292. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  293. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  294. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  295. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  296. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  297. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  298. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  299. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_examples_arbitrum.py +0 -0
  300. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_examples_base.py +0 -0
  301. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_examples_crosschain.py +0 -0
  302. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_external_state_executor_base.py +0 -0
  303. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  304. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  305. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  306. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_morpho_blue.py +0 -0
  307. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_pilot_v3.py +0 -0
  308. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  309. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  310. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  311. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  312. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_readers_integration.py +0 -0
  313. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_release_funds.py +0 -0
  314. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_swap_on_base.py +0 -0
  315. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  316. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_simulation_observation.py +0 -0
  317. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_solidity_compiler.py +0 -0
  318. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_solidity_mirrors.py +0 -0
  319. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_substrates.py +0 -0
  320. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_types_and_events.py +0 -0
  321. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_unpriceable_tokens.py +0 -0
  322. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_validation.py +0 -0
  323. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_vault_data_block.py +0 -0
  324. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_vault_dep_graph.py +0 -0
  325. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_vault_info_onchain_base.py +0 -0
  326. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  327. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_vault_simulator_blocks.py +0 -0
  328. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_web3_context.py +0 -0
  329. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/test_withdraw_manager.py +0 -0
  330. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/typed_client/consumer.py +0 -0
  331. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/tests/typed_client/rejected.py +0 -0
  332. {ipor_fusion-4.0.0.dev164 → ipor_fusion-4.0.0.dev171}/uv.lock +0 -0
@@ -32,9 +32,19 @@ uv run pytest -m "cli or mcp" --no-cov # fast offline sub
32
32
  uv run pytest --run-readiness -m readiness --no-cov -rA # opt-in live Ethereum-hub POC checks
33
33
  uv run python scripts/crosschain_readiness.py --output /tmp/crosschain-readiness.json # live Arbitrum/HyperEVM report
34
34
  uv run pytest tests/test_fuse_encoding.py -k aave --no-cov # single file / test
35
+ UPDATE_GOLDEN=1 uv run pytest -k golden --no-cov # rewrite pinned CLI/MCP output (tests/fixtures/*_golden/)
35
36
  uv lock --check # uv.lock in sync with pyproject.toml
36
37
  ```
37
38
 
39
+ **Golden output.** The exact text of `fusion vault info` (text and `--json`),
40
+ `fusion market morpho-blue|meta-morpho --json`, `fusion vault oracle-mapping
41
+ --json` and the matching MCP tools (`vault_info`, `market_morpho_blue`,
42
+ `market_meta_morpho`, `vault_oracle_mapping`, called through the server) is pinned
43
+ in `tests/fixtures/*_golden/` (`tests/_golden.py`, the `*_golden.py` tests), from
44
+ recorded inputs and no network. A change to any of them fails those tests;
45
+ regenerate with `UPDATE_GOLDEN=1` only when the change is intended and review the
46
+ fixture diff. Without it the tests only compare.
47
+
38
48
  Run format, check, pyright and pytest after every code change; that is exactly
39
49
  the sequence in `.github/workflows/python-build.yml`. When ruff `C901` trips,
40
50
  extract helpers, never suppress.
@@ -306,6 +306,14 @@ The internal `ipor_fusion.fuses.base` still re-exports them. The CLI's internal
306
306
  `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
307
307
  `vault_markets_view` and `oracle_mapping_view` (private names, not API).
308
308
 
309
+ **Event decoder modules renamed.** `ipor_fusion.fuses.external_state_events` and
310
+ `ipor_fusion.fuses.hypercore_events` are now `external_state_event_decoding` and
311
+ `hypercore_event_decoding`. The receipt decoder `hypercore_events`, re-exported
312
+ from `ipor_fusion.fuses`, hid its submodule of the same name (`import
313
+ ipor_fusion.fuses.hypercore_events as m` bound the function); the external-state
314
+ module was renamed to match. Import the decoders with `from ipor_fusion import
315
+ external_state_events, hypercore_events`; the modules are internal.
316
+
309
317
  **Removed.**
310
318
 
311
319
  - `IporFusionMarkets.RWA`: market 50 is `IporFusionMarkets.EXTERNAL_STATE`.
@@ -379,7 +387,7 @@ is the wrong one.
379
387
  from `IporFusionError`, so an `except IporFusionError` placed before their own
380
388
  handlers catches them first. All three live in `ipor_fusion.errors`, like every
381
389
  other SDK exception, and are exported from `ipor_fusion`; the modules that raise
382
- them (`ipor_fusion.morpho_api`, `ipor_fusion.fuses.external_state_events`,
390
+ them (`ipor_fusion.morpho_api`, `ipor_fusion.fuses.external_state_event_decoding`,
383
391
  `ipor_fusion.crosschain.ccip.indexer`) still import the same classes.
384
392
  - `AccessManager.read_owner(ctx)` raises the new `OwnerNotFoundError` (a
385
393
  `LookupError`) when no account holds `OWNER_ROLE`, instead of an `IndexError`.
@@ -951,5 +959,22 @@ are listed with a link.
951
959
  decoder cannot read raises the decoder's error. 3.x left it out of
952
960
  `result.observations` (so `result.get(label)` raised `KeyError`) or reported an
953
961
  undecodable read as `None`, indistinguishable from a read that returns `None`.
962
+ - **Check `success`; `error` is always set on failure.** A failed
963
+ `SimulatedCallResult` always carries a non-empty `error`: the node's message joined
964
+ with the decoded revert (`"execution reverted: NotAllowed"`), the decoded revert
965
+ alone when the node sent no message, else `"reverted without a reason"` (with the
966
+ return data's hex when it is too short to decode). 3.x could report `success=False`
967
+ with `error=None`, and `error` was the node's message only. A successful call's
968
+ `error` is `None`, and constructing one with an `error` raises `ValueError`.
969
+ `revert_reason` is `None` exactly when the call succeeded. The messages of
970
+ `SimulationResult.raise_for_failure` and `SimulationObservation.value`'s
971
+ `SimulationError` are `failed: {error}`; drop any `call.error or "reverted"`
972
+ fallback and match on `revert_reason`, not on the text of `error`.
973
+ - **`SimulatedCallResult` and `SimulationResult` are frozen**, so the rule above
974
+ holds after construction too: assigning a field raises
975
+ `dataclasses.FrozenInstanceError`. Build a changed copy with
976
+ `dataclasses.replace(result, ...)` (a failed call's `error` is completed again).
977
+ Both stay unhashable (`__hash__` is `None`, as before). The freeze is shallow:
978
+ the `logs` / `calls` lists themselves can still be mutated.
954
979
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
955
980
  a value that is not an address.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev164
3
+ Version: 4.0.0.dev171
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
@@ -191,9 +191,7 @@ def _assert_all_success(result: SimulationResult) -> None:
191
191
  if result.all_success:
192
192
  return
193
193
  failed = [(c.label, c.error) for c in result.failed_calls]
194
- raise AssertionError(
195
- f"simulation calls failed: {failed} (reason={result.revert_reason})"
196
- )
194
+ raise AssertionError(f"simulation calls failed: {failed}")
197
195
 
198
196
 
199
197
  def _connected_web3() -> Web3:
@@ -153,9 +153,7 @@ def _assert_all_success(result: SimulationResult) -> None:
153
153
  if result.all_success:
154
154
  return
155
155
  failed = [(call.label, call.error) for call in result.failed_calls]
156
- raise AssertionError(
157
- f"simulation calls failed: {failed} (reason={result.revert_reason})"
158
- )
156
+ raise AssertionError(f"simulation calls failed: {failed}")
159
157
 
160
158
 
161
159
  def _connected_web3() -> Web3:
@@ -634,7 +634,7 @@ def core_leg_previews(web3_hype: Web3) -> list[Preview]:
634
634
  # ── Part B: the hub legs, simulated at pinned blocks ─────────────────────────
635
635
  def _assert_relay_success(results: dict[ChainId, SimulationResult]) -> None:
636
636
  failed = [
637
- f"{chain}:{call.label}:{call.revert_reason}"
637
+ f"{chain}:{call.label}:{call.error}"
638
638
  for chain, result in results.items()
639
639
  for call in result.calls
640
640
  if not call.success
@@ -179,7 +179,7 @@ def _assert_relay_success(results: dict[ChainId, SimulationResult]) -> None:
179
179
  """Raise with a readable summary if any simulated call on either chain
180
180
  reverted; each entry names the call and decodes its own revert."""
181
181
  failed = [
182
- (int(chain_id), call.label, call.revert_reason or call.error)
182
+ (int(chain_id), call.label, call.error)
183
183
  for chain_id, result in results.items()
184
184
  for call in result.calls
185
185
  if not call.success
@@ -332,9 +332,7 @@ def _assert_all_success(result: SimulationResult) -> None:
332
332
  if result.all_success:
333
333
  return
334
334
  failed = [(c.label, c.error) for c in result.failed_calls]
335
- raise AssertionError(
336
- f"simulation calls failed: {failed} (reason={result.revert_reason})"
337
- )
335
+ raise AssertionError(f"simulation calls failed: {failed}")
338
336
 
339
337
 
340
338
  def _assert_only_expected_failure(result: SimulationResult, label: str) -> bytes:
@@ -159,9 +159,7 @@ def _assert_all_success(result: SimulationResult) -> None:
159
159
  if result.all_success:
160
160
  return
161
161
  failed = [(c.label, c.error) for c in result.failed_calls]
162
- raise AssertionError(
163
- f"simulation calls failed: {failed} (reason={result.revert_reason})"
164
- )
162
+ raise AssertionError(f"simulation calls failed: {failed}")
165
163
 
166
164
 
167
165
  def _connected_web3() -> Web3:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev164"
3
+ version = "4.0.0.dev171"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -315,7 +315,7 @@ from ipor_fusion.fuses import (
315
315
  precompile_index_of,
316
316
  system_address,
317
317
  )
318
- from ipor_fusion.fuses.external_state_events import (
318
+ from ipor_fusion.fuses.external_state_event_decoding import (
319
319
  EXTERNAL_STATE_EVENT_CLASSES,
320
320
  EXTERNAL_STATE_EVENT_SPECS,
321
321
  EXTERNAL_STATE_EVENTS,
@@ -343,7 +343,7 @@ from ipor_fusion.fuses.external_state_events import (
343
343
  external_state_event_name,
344
344
  external_state_events,
345
345
  )
346
- from ipor_fusion.fuses.hypercore_events import (
346
+ from ipor_fusion.fuses.hypercore_event_decoding import (
347
347
  HYPERCORE_EVENT_CLASSES,
348
348
  HyperCoreActionEnqueuedArgs,
349
349
  HyperCoreActionEnqueuedEvent,
@@ -142,8 +142,17 @@ class _Block:
142
142
  override_provider: StateOverrideProvider | None = None
143
143
 
144
144
 
145
- @dataclass(slots=True)
145
+ @dataclass(frozen=True, slots=True)
146
146
  class SimulatedCallResult:
147
+ """One simulated call. ``error`` is set exactly when ``success`` is False:
148
+ check ``success``, then read ``error`` for a non-empty, human-readable
149
+ failure (the node's message with the decoded revert reason, or
150
+ ``"reverted without a reason"`` plus any undecodable return data).
151
+ Construction enforces it: a failed call's ``error`` is completed from its
152
+ return data, and a successful call given an ``error`` raises
153
+ ``ValueError``. The record is frozen, so the invariant holds afterwards;
154
+ build a changed copy with `dataclasses.replace`."""
155
+
147
156
  label: str | None
148
157
  success: bool
149
158
  return_data: HexBytes
@@ -151,19 +160,33 @@ class SimulatedCallResult:
151
160
  error: str | None
152
161
  logs: list[dict]
153
162
  predicted_address: ChecksumAddress | None = None
163
+ # Frozen would add a __hash__ that fails on the logs list; stay unhashable.
164
+ __hash__ = None # type: ignore[assignment]
165
+
166
+ def __post_init__(self) -> None:
167
+ if self.success:
168
+ if self.error is not None:
169
+ raise ValueError(
170
+ f"successful simulated call {self.label!r} carries an error"
171
+ )
172
+ else:
173
+ # The record is frozen; completing the error is part of building it.
174
+ object.__setattr__(
175
+ self, "error", _failure_message(self.error, self.return_data)
176
+ )
154
177
 
155
178
  @property
156
179
  def revert_reason(self) -> str | None:
157
180
  """This call's decoded revert (a registered custom error by name, an
158
- ``Error(string)`` text or ``Panic``), ``None`` when it succeeded or
159
- reverted without data. ``SimulationResult.revert_reason`` is the
181
+ ``Error(string)`` text or ``Panic``), else its ``error``; ``None``
182
+ exactly when it succeeded. ``SimulationResult.revert_reason`` is the
160
183
  chain's first failed call, which may be a different one."""
161
184
  if self.success:
162
185
  return None
163
186
  return _decode_revert(self.return_data, self.error)
164
187
 
165
188
 
166
- @dataclass(slots=True)
189
+ @dataclass(frozen=True, slots=True)
167
190
  class SimulationResult:
168
191
  success: bool
169
192
  all_success: bool
@@ -172,6 +195,7 @@ class SimulationResult:
172
195
  execute_logs: list[dict]
173
196
  calls: list[SimulatedCallResult] = field(default_factory=list)
174
197
  failed_calls: list[SimulatedCallResult] = field(default_factory=list)
198
+ __hash__ = None # type: ignore[assignment]
175
199
 
176
200
  def raise_for_failure(self) -> None:
177
201
  """Raise :class:`SimulationError` naming the first failed call (label,
@@ -179,15 +203,11 @@ class SimulationResult:
179
203
  if not self.failed_calls:
180
204
  return
181
205
  first = self.failed_calls[0]
182
- reason = _decode_revert(first.return_data, first.error)
183
206
  label = first.label or "<unlabelled>"
184
- detail = first.error or "reverted"
185
- if reason and reason != first.error:
186
- detail = f"{detail}: {reason}"
187
207
  raise SimulationError(
188
- f"simulated call {label!r} failed: {detail}",
208
+ f"simulated call {label!r} failed: {first.error}",
189
209
  label=first.label,
190
- revert_reason=reason,
210
+ revert_reason=first.revert_reason,
191
211
  )
192
212
 
193
213
 
@@ -223,12 +243,10 @@ class SimulationObservation(Generic[T_co]):
223
243
  result = self.result
224
244
  name = self._label or "<unlabelled>"
225
245
  if not result.success:
226
- reason = result.revert_reason
227
246
  raise SimulationError(
228
- f"observation {name!r} failed: {result.error or 'reverted'}"
229
- + (f": {reason}" if reason and reason != result.error else ""),
247
+ f"observation {name!r} failed: {result.error}",
230
248
  label=self._label,
231
- revert_reason=reason,
249
+ revert_reason=result.revert_reason,
232
250
  )
233
251
  if not result.return_data:
234
252
  raise EmptyCallResultError(
@@ -732,7 +750,7 @@ class VaultSimulator:
732
750
  status = int(raw.get("status", "0x1"), 16)
733
751
  success = status == 1
734
752
  gas_used = int(raw.get("gasUsed", "0x0"), 16)
735
- error, return_data = _normalize_call_error(raw.get("error"), return_data)
753
+ message, return_data = _normalize_call_error(raw.get("error"), return_data)
736
754
  logs = raw.get("logs", []) or []
737
755
 
738
756
  call_result = SimulatedCallResult(
@@ -740,7 +758,7 @@ class VaultSimulator:
740
758
  success=success,
741
759
  return_data=return_data,
742
760
  gas_used=gas_used,
743
- error=error,
761
+ error=None if success else message,
744
762
  logs=logs,
745
763
  predicted_address=(source.predicted_address if success else None),
746
764
  )
@@ -753,14 +771,13 @@ class VaultSimulator:
753
771
  execute_gas += gas_used
754
772
  execute_logs.extend(logs)
755
773
  if not success and revert_reason is None:
756
- revert_reason = _decode_revert(return_data, error)
774
+ revert_reason = call_result.revert_reason
757
775
 
758
776
  failed_calls = [c for c in parsed if not c.success]
759
777
  all_success = not failed_calls
760
778
  if revert_reason is None and failed_calls:
761
779
  # No execute call reverted but a setup/observation did — surface it.
762
- first_failed = failed_calls[0]
763
- revert_reason = _decode_revert(first_failed.return_data, first_failed.error)
780
+ revert_reason = failed_calls[0].revert_reason
764
781
 
765
782
  return SimulationResult(
766
783
  success=execute_success,
@@ -910,6 +927,22 @@ def _normalize_call_error(
910
927
  return (error_raw if isinstance(error_raw, str) else None), return_data
911
928
 
912
929
 
930
+ def _failure_message(message: str | None, return_data: HexBytes) -> str:
931
+ """A failed call's ``error``: the node's message joined with the decoded
932
+ revert unless the message already carries it, else whichever exists, else
933
+ ``"reverted without a reason"`` with any return data too short to decode."""
934
+ reason = _decode_revert(return_data, None)
935
+ if message and reason:
936
+ return message if reason in message else f"{message}: {reason}"
937
+ if message:
938
+ return message
939
+ if reason:
940
+ return reason
941
+ if return_data:
942
+ return f"reverted without a reason (return data 0x{bytes(return_data).hex()})"
943
+ return "reverted without a reason"
944
+
945
+
913
946
  def _decode_revert(return_data: HexBytes, error: str | None) -> str | None:
914
947
  if return_data and len(return_data) >= 4:
915
948
  selector = bytes(return_data[:4])
@@ -49,7 +49,7 @@ from ipor_fusion.fuses.hypercore import (
49
49
  precompile_index_of,
50
50
  system_address,
51
51
  )
52
- from ipor_fusion.fuses.hypercore_events import (
52
+ from ipor_fusion.fuses.hypercore_event_decoding import (
53
53
  HYPERCORE_EVENT_CLASSES,
54
54
  HYPERCORE_EVENT_SPECS,
55
55
  HYPERCORE_EVENTS,
@@ -0,0 +1,69 @@
1
+ """Golden outputs: the exact text a CLI command or an MCP tool emits, pinned in
2
+ ``tests/fixtures/*_golden/``.
3
+
4
+ Regenerate them only when an output change is intended, then review the diff:
5
+
6
+ UPDATE_GOLDEN=1 uv run pytest -k golden --no-cov
7
+
8
+ Without ``UPDATE_GOLDEN`` (the default) a golden is only compared, never written.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import json
15
+ import os
16
+ from datetime import UTC, datetime, tzinfo
17
+ from pathlib import Path
18
+
19
+ from mcp.types import TextContent
20
+ from pydantic import BaseModel
21
+
22
+ from ipor_fusion.mcp.server import mcp
23
+
24
+ FIXTURES = Path(__file__).parent / "fixtures"
25
+
26
+ # The instant the goldens are rendered at: ages ("N days ago", `age_days`)
27
+ # are relative to it.
28
+ FROZEN_NOW = datetime(2026, 10, 1, tzinfo=UTC)
29
+
30
+
31
+ class FrozenDatetime(datetime):
32
+ """`datetime` whose `now()` is `FROZEN_NOW`; patched over a module's
33
+ `datetime` import so its rendering does not depend on the wall clock."""
34
+
35
+ @classmethod
36
+ def now(cls, tz: tzinfo | None = None) -> FrozenDatetime:
37
+ return cls.fromtimestamp(FROZEN_NOW.timestamp(), tz)
38
+
39
+
40
+ def assert_golden(path: Path, text: str) -> None:
41
+ """Compare ``text`` with the golden at ``path`` byte for byte; write it
42
+ instead when ``UPDATE_GOLDEN`` is set."""
43
+ if os.environ.get("UPDATE_GOLDEN"):
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ path.write_text(text, encoding="utf-8")
46
+ assert path.exists(), f"missing golden {path}; generate it with UPDATE_GOLDEN=1"
47
+ assert text == path.read_text(encoding="utf-8"), (
48
+ f"output differs from {path}; if the change is intended, regenerate "
49
+ "with UPDATE_GOLDEN=1 and review the diff"
50
+ )
51
+
52
+
53
+ def mcp_tool_text(
54
+ tool: str, arguments: dict[str, object], model: type[BaseModel]
55
+ ) -> str:
56
+ """Call ``tool`` through the MCP server, as a client does, and return the
57
+ text it emits. Checks that the text is ``model_dump_json(indent=2)`` of the
58
+ tool's response model and carries the same document as its structured
59
+ content."""
60
+ result = asyncio.run(mcp.call_tool(tool, arguments))
61
+ # A tool with a typed return answers (content blocks, structured content).
62
+ assert isinstance(result, tuple) and len(result) == 2
63
+ content, structured = result
64
+ assert isinstance(content, list) and len(content) == 1
65
+ block = content[0]
66
+ assert isinstance(block, TextContent)
67
+ assert json.loads(block.text) == structured
68
+ assert block.text == model.model_validate(structured).model_dump_json(indent=2)
69
+ return block.text
@@ -0,0 +1,200 @@
1
+ """In-memory stand-ins for the oracle mapping's chain reads: `FakeReader`
2
+ answers what `OracleMappingReader` would, `FakeVault` the three reads
3
+ `build_oracle_mapping` makes of the vault. No network, no RPC."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Generic, TypedDict, TypeVar
8
+
9
+ from web3 import Web3
10
+
11
+ from ipor_fusion.types import Price
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ def addr(n: int) -> str:
17
+ return Web3.to_checksum_address(f"0x{n:040x}")
18
+
19
+
20
+ class Feed(TypedDict, total=False):
21
+ """What a price feed answers; an absent key is a getter that reverts."""
22
+
23
+ morpho_oracle: str
24
+ collateral: str
25
+ loan: str
26
+ vault: str
27
+ round: tuple[int, int, int, int, int]
28
+ feed_decimals: int
29
+ description: str | None
30
+ version: int
31
+ aggregator: str
32
+ phase_id: int
33
+ asset_x: str
34
+ xy_feed: str
35
+ y_usd_feed: str
36
+
37
+
38
+ class Erc4626(TypedDict, total=False):
39
+ asset: str
40
+ decimals: int
41
+ rate: int
42
+
43
+
44
+ class FakeReader:
45
+ """In-memory stand-in for :class:`oracle_mapping.OracleMappingReader`."""
46
+
47
+ def __init__(self) -> None:
48
+ self.symbols: dict[str, str] = {}
49
+ self.decimals: dict[str, int] = {}
50
+ self.prices: dict[str, Price] = {}
51
+ self.sources: dict[str, str] = {}
52
+ self.feeds: dict[str, Feed] = {}
53
+ self.vaults: dict[str, Erc4626] = {}
54
+ self.morpho_prices: dict[str, int] = {}
55
+ # oracle variant (middleware-fallback resolution); "_mw" because the
56
+ # exact method name would be shadowed by the attribute
57
+ self.underlying_mw: str | None = None
58
+ self.registry: str | None = None
59
+ self.delegated: FakeReader | None = None
60
+ # enumeration (configured_assets / asset_source_events)
61
+ self.configured: list[str] | None = None
62
+ self.events: list[tuple[int, str, str]] = []
63
+ self.events_to_block: int | None = None
64
+
65
+ # token metadata
66
+ def symbol(self, a: str) -> str | None:
67
+ return self.symbols.get(a)
68
+
69
+ def token_decimals(self, a: str) -> int | None:
70
+ return self.decimals.get(a)
71
+
72
+ # oracle
73
+ def asset_price(self, a: str) -> Price | None:
74
+ return self.prices.get(a)
75
+
76
+ def source_of(self, a: str) -> str | None:
77
+ return self.sources.get(a)
78
+
79
+ # oracle variant probes
80
+ def underlying_middleware(self) -> str | None:
81
+ return self.underlying_mw
82
+
83
+ def chainlink_feed_registry(self) -> str | None:
84
+ return self.registry
85
+
86
+ def delegate(self, oracle: str) -> FakeReader:
87
+ assert oracle == self.underlying_mw # must delegate to the probed middleware
88
+ assert self.delegated is not None # fail fast on a misconfigured test
89
+ return self.delegated
90
+
91
+ # feed probes
92
+ def _f(self, s: str) -> Feed:
93
+ return self.feeds.get(s, {})
94
+
95
+ def feed_morpho_oracle(self, s: str) -> str | None:
96
+ return self._f(s).get("morpho_oracle")
97
+
98
+ def feed_collateral_token(self, s: str) -> str | None:
99
+ return self._f(s).get("collateral")
100
+
101
+ def feed_loan_token(self, s: str) -> str | None:
102
+ return self._f(s).get("loan")
103
+
104
+ def feed_vault(self, s: str) -> str | None:
105
+ return self._f(s).get("vault")
106
+
107
+ def feed_latest_round_data(self, s: str) -> tuple[int, int, int, int, int] | None:
108
+ return self._f(s).get("round")
109
+
110
+ def feed_decimals(self, s: str) -> int | None:
111
+ return self._f(s).get("feed_decimals")
112
+
113
+ def feed_description(self, s: str) -> str | None:
114
+ return self._f(s).get("description")
115
+
116
+ def feed_version(self, s: str) -> int | None:
117
+ return self._f(s).get("version")
118
+
119
+ def feed_aggregator(self, s: str) -> str | None:
120
+ return self._f(s).get("aggregator")
121
+
122
+ def feed_phase_id(self, s: str) -> int | None:
123
+ return self._f(s).get("phase_id")
124
+
125
+ def morpho_oracle_price(self, o: str) -> int | None:
126
+ return self.morpho_prices.get(o)
127
+
128
+ def feed_asset_x(self, s: str) -> str | None:
129
+ return self._f(s).get("asset_x")
130
+
131
+ def feed_asset_x_asset_y_feed(self, s: str) -> str | None:
132
+ return self._f(s).get("xy_feed")
133
+
134
+ def feed_asset_y_usd_feed(self, s: str) -> str | None:
135
+ return self._f(s).get("y_usd_feed")
136
+
137
+ # erc4626 vault
138
+ def vault_asset(self, v: str) -> str | None:
139
+ return self.vaults.get(v, {}).get("asset")
140
+
141
+ def vault_decimals(self, v: str) -> int | None:
142
+ return self.vaults.get(v, {}).get("decimals")
143
+
144
+ def vault_convert_to_assets(self, v: str, shares: int) -> int | None:
145
+ return self.vaults.get(v, {}).get("rate")
146
+
147
+ # enumeration
148
+ def configured_assets(self) -> list[str] | None:
149
+ return self.configured
150
+
151
+ def asset_source_events(self, to_block: int) -> list[tuple[int, str, str]]:
152
+ self.events_to_block = to_block
153
+ return self.events
154
+
155
+
156
+ def chainlink_asset(r: FakeReader, asset: str, source: str, *, symbol: str) -> None:
157
+ r.symbols[asset] = symbol
158
+ r.decimals[asset] = 6
159
+ r.sources[asset] = source
160
+ r.prices[asset] = Price(asset=asset, amount=99_980_000, decimals=8)
161
+ # full confirmed-tier evidence: description + version on top of the round
162
+ r.feeds[source] = {
163
+ "round": (1, 99_980_000, 0, 1_700_000_000, 1),
164
+ "feed_decimals": 8,
165
+ "description": f"{symbol} / USD",
166
+ "version": 4,
167
+ }
168
+
169
+
170
+ class Returns(Generic[T]):
171
+ """Wraps a value as a Call-like object exposing .call(ctx)."""
172
+
173
+ def __init__(self, value: T):
174
+ self._value = value
175
+
176
+ def call(self, ctx: object) -> T:
177
+ return self._value
178
+
179
+
180
+ class FakeVault:
181
+ """Stand-in for PlasmaVault — only the three reads build_oracle_mapping makes."""
182
+
183
+ def __init__(
184
+ self, oracle: str, asset: str, name: str, *, name_raises: bool = False
185
+ ):
186
+ self._oracle = oracle
187
+ self._asset = asset
188
+ self._name = name
189
+ self._name_raises = name_raises
190
+
191
+ def read_price_oracle_address(self, ctx: object) -> str:
192
+ return self._oracle
193
+
194
+ def underlying_asset_address(self) -> Returns[str]:
195
+ return Returns(self._asset)
196
+
197
+ def name(self) -> Returns[str]:
198
+ if self._name_raises:
199
+ raise RuntimeError("name() reverted")
200
+ return Returns(self._name)