ipor-fusion 4.0.0.dev249__tar.gz → 4.0.0.dev255__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 (342) hide show
  1. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/PKG-INFO +2 -2
  2. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/README.md +1 -1
  3. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/pyproject.toml +1 -1
  4. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/__init__.py +2 -0
  5. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/__init__.py +2 -0
  6. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/access.py +57 -1
  7. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/aave_v3.py +75 -19
  8. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/morpho.py +66 -14
  9. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_data.py +7 -2
  10. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_positions.py +30 -33
  11. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_access_manager.py +46 -0
  12. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_helpers.py +38 -27
  13. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_public_api.py +2 -0
  14. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_reader_encoding.py +73 -1
  15. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.claude-plugin/marketplace.json +0 -0
  16. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.claude-plugin/plugin.json +0 -0
  17. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.env.example +0 -0
  18. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.gemini/settings.json +0 -0
  19. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.git-blame-ignore-revs +0 -0
  20. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/cd.yml +0 -0
  21. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/ci.yml +0 -0
  22. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/commit-lint.yml +0 -0
  23. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/notify-slack.yml +0 -0
  24. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/python-build.yml +0 -0
  25. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/python-publish.yml +0 -0
  26. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/release.yml +0 -0
  27. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.github/workflows/report-slack.yml +0 -0
  28. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.gitignore +0 -0
  29. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.mcp.json +0 -0
  30. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.pre-commit-config.yaml +0 -0
  31. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/.python-version +0 -0
  32. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/AGENTS.md +0 -0
  33. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/CHANGELOG.md +0 -0
  34. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/CLAUDE.md +0 -0
  35. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/CODEOWNERS +0 -0
  36. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/LICENSE +0 -0
  37. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/MIGRATION_4.md +0 -0
  38. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/build.sh +0 -0
  39. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/CONTRIBUTING.md +0 -0
  40. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/README.md +0 -0
  41. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  42. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  43. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  44. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  45. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/external_state_margin_leg_base.py +0 -0
  46. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/examples/simple_aave_v3_supply_base.py +0 -0
  47. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/pytest.ini +0 -0
  48. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/scripts/commit-check.sh +0 -0
  49. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/scripts/crosschain_readiness.py +0 -0
  50. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/skills/ipor-deploy-vault/SKILL.md +0 -0
  51. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/about.py +0 -0
  52. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cache.py +0 -0
  53. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/chains.py +0 -0
  54. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/__init__.py +0 -0
  55. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  56. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/config_cmd.py +0 -0
  57. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/config_store.py +0 -0
  58. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/main.py +0 -0
  59. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/market_cmd.py +0 -0
  60. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/options.py +0 -0
  61. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  62. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  63. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/vault_health.py +0 -0
  64. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  65. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  66. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  67. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/config/__init__.py +0 -0
  68. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/config/roles.py +0 -0
  69. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/address.py +0 -0
  70. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/context.py +0 -0
  71. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/contract.py +0 -0
  72. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/erc20.py +0 -0
  73. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/external_state_executor.py +0 -0
  74. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/fee_manager.py +0 -0
  75. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/fuse.py +0 -0
  76. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/fuse_action.py +0 -0
  77. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/fusion_factory.py +0 -0
  78. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/logs.py +0 -0
  79. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/multicall.py +0 -0
  80. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/oracle.py +0 -0
  81. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/plasma_vault.py +0 -0
  82. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/rewards_manager.py +0 -0
  83. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/simulation.py +0 -0
  84. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/simulation_result.py +0 -0
  85. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  86. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/__init__.py +0 -0
  87. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  88. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  89. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  90. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  91. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  92. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  93. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  94. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  95. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  96. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/contracts.py +0 -0
  97. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/discovery.py +0 -0
  98. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/errors.py +0 -0
  99. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  100. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  101. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  102. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  103. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/lane.py +0 -0
  104. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/logs.py +0 -0
  105. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/messages.py +0 -0
  106. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/simulation.py +0 -0
  107. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  108. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  109. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  110. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  111. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  112. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/crosschain/transport.py +0 -0
  113. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/dependency_graph.py +0 -0
  114. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/entry_cli.py +0 -0
  115. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/entry_mcp.py +0 -0
  116. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/errors.py +0 -0
  117. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/explorer.py +0 -0
  118. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/field_docs.py +0 -0
  119. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/__init__.py +0 -0
  120. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  121. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/async_action.py +0 -0
  122. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/base.py +0 -0
  123. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  124. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/erc4626.py +0 -0
  125. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  126. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/events.py +0 -0
  127. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/external_state.py +0 -0
  128. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
  129. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  130. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  131. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/hypercore.py +0 -0
  132. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
  133. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/merkl.py +0 -0
  134. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/morpho.py +0 -0
  135. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  136. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  137. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/fuses/universal.py +0 -0
  138. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/guide/__init__.py +0 -0
  139. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/guide/architecture.md +0 -0
  140. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/guide/glossary.md +0 -0
  141. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/guide/invariants.md +0 -0
  142. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/guide/quickstart.md +0 -0
  143. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/hypercore/__init__.py +0 -0
  144. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/market_ids.py +0 -0
  145. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/market_info.py +0 -0
  146. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/mcp/__init__.py +0 -0
  147. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/mcp/guide.py +0 -0
  148. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/mcp/models.py +0 -0
  149. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/mcp/server.py +0 -0
  150. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/morpho_api.py +0 -0
  151. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/py.typed +0 -0
  152. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/__init__.py +0 -0
  153. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/aave_v4.py +0 -0
  154. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/compound_v3.py +0 -0
  155. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/hypercore.py +0 -0
  156. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  157. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/lending_health.py +0 -0
  158. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  159. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  160. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  161. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/position_manager.py +0 -0
  162. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  163. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  164. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/readers/unpriceable.py +0 -0
  165. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/substrates.py +0 -0
  166. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/types.py +0 -0
  167. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/validation.py +0 -0
  168. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_erc20.py +0 -0
  169. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_info.py +0 -0
  170. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_managers.py +0 -0
  171. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/src/ipor_fusion/vault_reads.py +0 -0
  172. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/UniswapV3UniversalRouter.py +0 -0
  173. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_crosschain.py +0 -0
  174. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_crosschain_lifecycle.py +0 -0
  175. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_crosschain_recovery.py +0 -0
  176. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_euler_v2.py +0 -0
  177. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_event_args.py +0 -0
  178. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_fake_rpc.py +0 -0
  179. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_foundry.py +0 -0
  180. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_golden.py +0 -0
  181. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_guide.py +0 -0
  182. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_hypercore_model.py +0 -0
  183. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_hypercore_shadow.py +0 -0
  184. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_live.py +0 -0
  185. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_multicall.py +0 -0
  186. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_oracle_mapping_fake.py +0 -0
  187. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_simulate.py +0 -0
  188. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/_validation.py +0 -0
  189. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/addresses.py +0 -0
  190. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/conftest.py +0 -0
  191. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/constants.py +0 -0
  192. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  193. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/hypercore_market55.json +0 -0
  194. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/meta_morpho_v1.json +0 -0
  195. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +0 -0
  196. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/meta_morpho_v2.json +0 -0
  197. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +0 -0
  198. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_api/market.json +0 -0
  199. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_api/vault_v1.json +0 -0
  200. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_api/vault_v2.json +0 -0
  201. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +0 -0
  202. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue.json +0 -0
  203. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue.mcp.json +0 -0
  204. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue_api_error.json +0 -0
  205. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +0 -0
  206. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue_no_api.json +0 -0
  207. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +0 -0
  208. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/oracle_mapping_golden/mapping.json +0 -0
  209. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +0 -0
  210. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/simulate_v1_request.json +0 -0
  211. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/clean.json +0 -0
  212. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/clean.mcp.json +0 -0
  213. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  214. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/deployment_error.json +0 -0
  215. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/deployment_error.mcp.json +0 -0
  216. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/deployment_error.txt +0 -0
  217. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/fetched.cli.json +0 -0
  218. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/fetched.json +0 -0
  219. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/fetched.mcp.json +0 -0
  220. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/full.json +0 -0
  221. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/full.mcp.json +0 -0
  222. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/full.txt +0 -0
  223. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/rich.json +0 -0
  224. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/rich.mcp.json +0 -0
  225. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  226. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_about.py +0 -0
  227. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cache.py +0 -0
  228. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_chains.py +0 -0
  229. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_changelog.py +0 -0
  230. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_commands.py +0 -0
  231. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_config_store.py +0 -0
  232. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_hypercore.py +0 -0
  233. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_lending_health.py +0 -0
  234. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_market_golden.py +0 -0
  235. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_market_morpho_blue.py +0 -0
  236. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_mcp_parity.py +0 -0
  237. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_oracle_mapping.py +0 -0
  238. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_oracle_mapping_golden.py +0 -0
  239. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_pending_requests_scan.py +0 -0
  240. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_role_accounts.py +0 -0
  241. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_vault_info_golden.py +0 -0
  242. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_vault_predates_getters.py +0 -0
  243. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_cli_vault_validate.py +0 -0
  244. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_contract_call.py +0 -0
  245. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_contract_encoder.py +0 -0
  246. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_ccip_events.py +0 -0
  247. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_ccip_indexer.py +0 -0
  248. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_ccip_quote.py +0 -0
  249. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_chainlink.py +0 -0
  250. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_contracts.py +0 -0
  251. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_encoding.py +0 -0
  252. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_errors.py +0 -0
  253. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_lane.py +0 -0
  254. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_layering.py +0 -0
  255. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_readiness.py +0 -0
  256. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_readiness_report.py +0 -0
  257. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_crosschain_transport.py +0 -0
  258. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_docs_typing.py +0 -0
  259. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_erc20.py +0 -0
  260. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_error_hierarchy.py +0 -0
  261. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_examples_composed_hypercore.py +0 -0
  262. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_examples_crosschain.py +0 -0
  263. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  264. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_examples_vaults.py +0 -0
  265. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_explorer.py +0 -0
  266. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_external_state_events.py +0 -0
  267. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_external_state_executor.py +0 -0
  268. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_fee_manager.py +0 -0
  269. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_fuse_encoding.py +0 -0
  270. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_fusion_instance.py +0 -0
  271. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_guide.py +0 -0
  272. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_deployment.py +0 -0
  273. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_events.py +0 -0
  274. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_flow.py +0 -0
  275. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_live.py +0 -0
  276. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_outcome.py +0 -0
  277. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_readers.py +0 -0
  278. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_hypercore_simulation.py +0 -0
  279. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_logs.py +0 -0
  280. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_mcp_hypercore.py +0 -0
  281. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_mcp_models.py +0 -0
  282. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_mcp_server.py +0 -0
  283. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_mcp_vault_validate.py +0 -0
  284. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_multicall.py +0 -0
  285. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_oracle.py +0 -0
  286. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_oracle_mapping.py +0 -0
  287. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_plasma_vault.py +0 -0
  288. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_public_type_reachability.py +0 -0
  289. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_readiness_selection.py +0 -0
  290. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_revert_decoding.py +0 -0
  291. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_rewards_manager.py +0 -0
  292. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_roles.py +0 -0
  293. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_sdk_layering.py +0 -0
  294. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  295. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  296. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  297. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_crosschain_canary.py +0 -0
  298. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_crosschain_hypercore.py +0 -0
  299. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  300. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  301. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  302. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  303. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  304. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  305. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  306. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_examples_arbitrum.py +0 -0
  307. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_examples_base.py +0 -0
  308. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_examples_crosschain.py +0 -0
  309. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_external_state_executor_base.py +0 -0
  310. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  311. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  312. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  313. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_morpho_blue.py +0 -0
  314. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_pilot_v3.py +0 -0
  315. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  316. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  317. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  318. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  319. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_readers_integration.py +0 -0
  320. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_release_funds.py +0 -0
  321. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_swap_on_base.py +0 -0
  322. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  323. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulation_errors.py +0 -0
  324. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_simulation_observation.py +0 -0
  325. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_solidity_compiler.py +0 -0
  326. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_solidity_mirrors.py +0 -0
  327. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_substrates.py +0 -0
  328. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_types_and_events.py +0 -0
  329. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_unpriceable_tokens.py +0 -0
  330. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_validation.py +0 -0
  331. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_data_block.py +0 -0
  332. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_dep_graph.py +0 -0
  333. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_info_fetched_golden.py +0 -0
  334. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_info_onchain_base.py +0 -0
  335. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  336. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_reconciliation_onchain_base.py +0 -0
  337. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_vault_simulator_blocks.py +0 -0
  338. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_web3_context.py +0 -0
  339. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/test_withdraw_manager.py +0 -0
  340. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/typed_client/consumer.py +0 -0
  341. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/tests/typed_client/rejected.py +0 -0
  342. {ipor_fusion-4.0.0.dev249 → ipor_fusion-4.0.0.dev255}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev249
3
+ Version: 4.0.0.dev255
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
@@ -325,7 +325,7 @@ Reverts on the deploy-and-configure path, keyed by selector so a failed transact
325
325
  | `0x8745fbfd` | `DaoFeePackagesArrayEmpty()` | `clone()` was sent to `IporFusionFactoryImpl` | Send it to `IporFusionFactoryProxy` for that chain |
326
326
  | `0x9996b315` | `AddressEmptyCode(address)` | `execute()` touched a market with no balance fuse | `add_balance_fuse(market_id, balance_fuse)` before the first `execute` on that market |
327
327
  | `0x4bf4de4e` | `HandlerNotFound()` | A protocol called back into the vault during `execute()` (a flash loan) and no handler is registered for its address and selector | `update_callback_handler(handler, sender, selector)` before the first `execute` that triggers the callback |
328
- | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | `AccessManager.grant_role(role, account, Period(0))` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
328
+ | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | Check first with `AccessManager.can_call(caller, target, selector).call(ctx)` (a closed target or a missing role answers `immediate=False, delay=0`; a role with an execution delay answers its `delay`). `AccessManager.grant_role(role, account, Period(0))` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
329
329
  | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send(ctx)` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
330
330
 
331
331
  `clone()` grants the owner only `Roles.OWNER_ROLE` (1). OWNER grants `Roles.ATOMIST_ROLE` (100), which administers `Roles.FUSE_MANAGER_ROLE` (300, configuration), `Roles.ALPHA_ROLE` (200, `execute`) and `Roles.WHITELIST_ROLE` (800, `deposit` on a private vault).
@@ -277,7 +277,7 @@ Reverts on the deploy-and-configure path, keyed by selector so a failed transact
277
277
  | `0x8745fbfd` | `DaoFeePackagesArrayEmpty()` | `clone()` was sent to `IporFusionFactoryImpl` | Send it to `IporFusionFactoryProxy` for that chain |
278
278
  | `0x9996b315` | `AddressEmptyCode(address)` | `execute()` touched a market with no balance fuse | `add_balance_fuse(market_id, balance_fuse)` before the first `execute` on that market |
279
279
  | `0x4bf4de4e` | `HandlerNotFound()` | A protocol called back into the vault during `execute()` (a flash loan) and no handler is registered for its address and selector | `update_callback_handler(handler, sender, selector)` before the first `execute` that triggers the callback |
280
- | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | `AccessManager.grant_role(role, account, Period(0))` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
280
+ | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | Check first with `AccessManager.can_call(caller, target, selector).call(ctx)` (a closed target or a missing role answers `immediate=False, delay=0`; a role with an execution delay answers its `delay`). `AccessManager.grant_role(role, account, Period(0))` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
281
281
  | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send(ctx)` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
282
282
 
283
283
  `clone()` grants the owner only `Roles.OWNER_ROLE` (1). OWNER grants `Roles.ATOMIST_ROLE` (100), which administers `Roles.FUSE_MANAGER_ROLE` (300, configuration), `Roles.ALPHA_ROLE` (200, `execute`) and `Roles.WHITELIST_ROLE` (800, `deposit` on a private vault).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev249"
3
+ version = "4.0.0.dev255"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -26,6 +26,7 @@ from ipor_fusion.chains import (
26
26
  from ipor_fusion.config.roles import Roles
27
27
  from ipor_fusion.core.access import (
28
28
  AccessManager,
29
+ CallPermission,
29
30
  RoleAccount,
30
31
  RoleAccountJson,
31
32
  RoleAccountsJson,
@@ -464,6 +465,7 @@ __all__ = [
464
465
  "AccessManager",
465
466
  "RoleAccount",
466
467
  "RoleAccountJson",
468
+ "CallPermission",
467
469
  "RoleStatus",
468
470
  "resolve_access_manager",
469
471
  "role_account_sort_key",
@@ -1,5 +1,6 @@
1
1
  from ipor_fusion.core.access import (
2
2
  AccessManager,
3
+ CallPermission,
3
4
  RoleAccount,
4
5
  RoleAccountJson,
5
6
  RoleAccountsJson,
@@ -92,6 +93,7 @@ __all__ = [
92
93
  "AccessManager",
93
94
  "RoleAccount",
94
95
  "RoleAccountJson",
96
+ "CallPermission",
95
97
  "RoleStatus",
96
98
  "RewardsManager",
97
99
  "VestingData",
@@ -8,7 +8,7 @@ from eth_typing import ChecksumAddress
8
8
  from hexbytes import HexBytes
9
9
  from web3 import Web3
10
10
  from web3.exceptions import ContractLogicError
11
- from web3.types import LogReceipt
11
+ from web3.types import LogReceipt, Timestamp
12
12
 
13
13
  from ipor_fusion.config.roles import Roles
14
14
  from ipor_fusion.core.context import Web3Context
@@ -48,6 +48,30 @@ class RoleStatus:
48
48
  execution_delay: Period
49
49
 
50
50
 
51
+ @dataclass(frozen=True, slots=True)
52
+ class CallPermission:
53
+ """`AccessManager.canCall`: whether ``caller`` may call ``selector`` on ``target``.
54
+
55
+ ``immediate`` is a call allowed now; a member with an execution delay gets
56
+ ``immediate=False`` and that ``delay`` (it must schedule the call first). A closed
57
+ target or a non-member answers ``(False, 0)``. This is the role check only: the
58
+ IPOR manager's redemption lock and the target's own logic can still revert.
59
+ """
60
+
61
+ immediate: bool
62
+ delay: Period
63
+
64
+ @property
65
+ def allowed(self) -> bool:
66
+ """Allowed now or after scheduling."""
67
+ return self.immediate or self.delay > 0
68
+
69
+
70
+ def _call_permission_decoder(values: tuple[bool, int]) -> CallPermission:
71
+ immediate, delay = values
72
+ return CallPermission(immediate=immediate, delay=Period(delay))
73
+
74
+
51
75
  @dataclass(slots=True)
52
76
  class RoleAccount:
53
77
  """Account-role membership record returned by AccessManager queries."""
@@ -159,6 +183,38 @@ class AccessManager(ContractWrapper):
159
183
  decoder=_role_status_decoder,
160
184
  )
161
185
 
186
+ def can_call(
187
+ self, caller: ChecksumAddress, target: ChecksumAddress, selector: bytes
188
+ ) -> Call[CallPermission]:
189
+ """`canCall(caller, target, selector)`: the closed-target check, the
190
+ function's role and the caller's membership and delay in one read."""
191
+ return self._view(
192
+ "canCall(address,address,bytes4)",
193
+ caller,
194
+ target,
195
+ selector,
196
+ output_types=["bool", "uint32"],
197
+ decoder=_call_permission_decoder,
198
+ )
199
+
200
+ def is_target_closed(self, target: ChecksumAddress) -> Call[bool]:
201
+ """Whether the admin closed ``target``: every restricted call to it fails."""
202
+ return self._view(
203
+ "isTargetClosed(address)", target, output_types=["bool"], decoder=bool
204
+ )
205
+
206
+ def get_account_lock_time(self, account: ChecksumAddress) -> Call[Timestamp]:
207
+ """Unix time until which ``account``, as the caller, cannot withdraw, redeem
208
+ or transfer shares (IPOR redemption delay). A deposit, mint or
209
+ ``depositWithPermit`` sets it for its caller, not for the share receiver;
210
+ a time ``<= block.timestamp`` is unlocked."""
211
+ return self._view(
212
+ "getAccountLockTime(address)",
213
+ account,
214
+ output_types=["uint256"],
215
+ decoder=Timestamp,
216
+ )
217
+
162
218
  # ── Composite reads on ``ctx``: event replay + batched hasRole reads ───
163
219
 
164
220
  def read_owner(self, ctx: Web3Context) -> ChecksumAddress:
@@ -1,3 +1,4 @@
1
+ from collections.abc import Sequence
1
2
  from dataclasses import dataclass
2
3
 
3
4
  from eth_typing import ChecksumAddress
@@ -174,26 +175,81 @@ class AaveV3Reader(ContractWrapper):
174
175
  zero when `asset` is not listed on this Pool.
175
176
  """
176
177
  tokens = self.reserve_tokens(asset).call(ctx)
177
- reserve_tokens = (
178
- tokens.a_token,
179
- tokens.variable_debt_token,
180
- tokens.stable_debt_token,
178
+ listed = _listed_tokens(tokens)
179
+ balances = Multicall3(ctx).aggregate(
180
+ [ERC20(token).balance_of(user) for token in listed]
181
181
  )
182
- listed = [token for token in reserve_tokens if token.lower() != ZERO_ADDRESS]
183
- balances = iter(
184
- Multicall3(ctx).aggregate(
185
- [ERC20(token).balance_of(user) for token in listed]
186
- )
182
+ return _position_breakdown(
183
+ asset, tokens, dict(zip(listed, balances, strict=True))
187
184
  )
188
- supply, variable_debt, stable_debt = (
189
- next(balances) if token in listed else Amount(0) for token in reserve_tokens
185
+
186
+ def read_position_breakdowns(
187
+ self,
188
+ ctx: Web3Context,
189
+ assets: Sequence[ChecksumAddress],
190
+ user: ChecksumAddress,
191
+ ) -> list[AaveV3PositionBreakdown | None]:
192
+ """:meth:`read_position_breakdown` for every asset in two Multicall3 batches
193
+ (every reserve's tokens, then every balance), each one round trip per
194
+ batch size, or one read per call without Multicall3; in ``assets`` order.
195
+
196
+ ``None`` for an asset whose reserve data or a balance reverts, returns no
197
+ data or cannot be decoded; a transport error raises.
198
+ """
199
+ multicall = Multicall3(ctx)
200
+ reserves = multicall.try_aggregate([self.reserve_tokens(a) for a in assets])
201
+ listed = {
202
+ token
203
+ for tokens in reserves
204
+ if tokens is not None
205
+ for token in _listed_tokens(tokens)
206
+ }
207
+ ordered = sorted(listed)
208
+ read = multicall.try_aggregate(
209
+ [ERC20(token).balance_of(user) for token in ordered]
190
210
  )
191
- return AaveV3PositionBreakdown(
192
- asset=asset,
193
- a_token=tokens.a_token,
194
- variable_debt_token=tokens.variable_debt_token,
195
- stable_debt_token=tokens.stable_debt_token,
196
- supply=supply,
197
- variable_debt=variable_debt,
198
- stable_debt=stable_debt,
211
+ balances = {
212
+ token: balance
213
+ for token, balance in zip(ordered, read, strict=True)
214
+ if balance is not None
215
+ }
216
+ return [
217
+ _position_breakdown(asset, tokens, balances)
218
+ if tokens is not None
219
+ and all(token in balances for token in _listed_tokens(tokens))
220
+ else None
221
+ for asset, tokens in zip(assets, reserves, strict=True)
222
+ ]
223
+
224
+
225
+ def _listed_tokens(tokens: AaveV3ReserveTokens) -> list[ChecksumAddress]:
226
+ return [
227
+ token
228
+ for token in (
229
+ tokens.a_token,
230
+ tokens.variable_debt_token,
231
+ tokens.stable_debt_token,
199
232
  )
233
+ if token.lower() != ZERO_ADDRESS
234
+ ]
235
+
236
+
237
+ def _position_breakdown(
238
+ asset: ChecksumAddress,
239
+ tokens: AaveV3ReserveTokens,
240
+ balances: dict[ChecksumAddress, Amount],
241
+ ) -> AaveV3PositionBreakdown:
242
+ """A zero token address counts as a zero balance."""
243
+
244
+ def balance(token: ChecksumAddress) -> Amount:
245
+ return Amount(0) if token.lower() == ZERO_ADDRESS else balances[token]
246
+
247
+ return AaveV3PositionBreakdown(
248
+ asset=asset,
249
+ a_token=tokens.a_token,
250
+ variable_debt_token=tokens.variable_debt_token,
251
+ stable_debt_token=tokens.stable_debt_token,
252
+ supply=balance(tokens.a_token),
253
+ variable_debt=balance(tokens.variable_debt_token),
254
+ stable_debt=balance(tokens.stable_debt_token),
255
+ )
@@ -1,4 +1,5 @@
1
1
  import math
2
+ from collections.abc import Sequence
2
3
  from dataclasses import dataclass
3
4
 
4
5
  from eth_abi import encode
@@ -225,22 +226,45 @@ class MorphoReader(ContractWrapper):
225
226
  market_read = batch.add(self.market(market_id))
226
227
  params_read = batch.add(self.market_params(market_id))
227
228
  batch.run()
228
- pos, market, params = pos_read.value, market_read.value, params_read.value
229
- borrow_assets = to_assets_up(
230
- pos.borrow_shares, market.total_borrow_assets, market.total_borrow_shares
231
- )
232
- supply_assets = to_assets_down(
233
- pos.supply_shares, market.total_supply_assets, market.total_supply_shares
234
- )
235
- return MorphoPositionBreakdown(
236
- market_id=market_id,
237
- loan_token=params.loan_token,
238
- collateral_token=params.collateral_token,
239
- collateral=pos.collateral,
240
- borrow_assets=Amount(borrow_assets),
241
- supply_assets=Amount(supply_assets),
229
+ return _position_breakdown(
230
+ market_id, pos_read.value, market_read.value, params_read.value
242
231
  )
243
232
 
233
+ def read_position_breakdowns(
234
+ self,
235
+ ctx: Web3Context,
236
+ market_ids: Sequence[MorphoBlueMarketId],
237
+ user: ChecksumAddress,
238
+ ) -> list[MorphoPositionBreakdown | None]:
239
+ """:meth:`read_position_breakdown` for every market in one Multicall3
240
+ batch (one round trip per batch size, or one read per call without
241
+ Multicall3), in ``market_ids`` order.
242
+
243
+ ``None`` for a market whose position, state or params read reverts,
244
+ returns no data or cannot be decoded; a transport error raises.
245
+ """
246
+ batch = Multicall3(ctx).batch()
247
+ reads = [
248
+ (
249
+ batch.try_add(self.position(market_id, user)),
250
+ batch.try_add(self.market(market_id)),
251
+ batch.try_add(self.market_params(market_id)),
252
+ )
253
+ for market_id in market_ids
254
+ ]
255
+ batch.run()
256
+ breakdowns: list[MorphoPositionBreakdown | None] = []
257
+ for market_id, (pos, market, params) in zip(market_ids, reads, strict=True):
258
+ if pos.value is None or market.value is None or params.value is None:
259
+ breakdowns.append(None)
260
+ else:
261
+ breakdowns.append(
262
+ _position_breakdown(
263
+ market_id, pos.value, market.value, params.value
264
+ )
265
+ )
266
+ return breakdowns
267
+
244
268
  def market_params(self, market_id: MorphoBlueMarketId) -> Call[MorphoMarketParams]:
245
269
  return self._view(
246
270
  "idToMarketParams(bytes32)",
@@ -352,3 +376,31 @@ def _irm_borrow_rate_view(
352
376
  to=params.irm, data=selector + payload, output_types=["uint256"]
353
377
  )
354
378
  return call.call(ctx)
379
+
380
+
381
+ def _position_breakdown(
382
+ market_id: MorphoBlueMarketId,
383
+ pos: MorphoPosition,
384
+ market: MorphoMarket,
385
+ params: MorphoMarketParams,
386
+ ) -> MorphoPositionBreakdown:
387
+ return MorphoPositionBreakdown(
388
+ market_id=market_id,
389
+ loan_token=params.loan_token,
390
+ collateral_token=params.collateral_token,
391
+ collateral=pos.collateral,
392
+ borrow_assets=Amount(
393
+ to_assets_up(
394
+ pos.borrow_shares,
395
+ market.total_borrow_assets,
396
+ market.total_borrow_shares,
397
+ )
398
+ ),
399
+ supply_assets=Amount(
400
+ to_assets_down(
401
+ pos.supply_shares,
402
+ market.total_supply_assets,
403
+ market.total_supply_shares,
404
+ )
405
+ ),
406
+ )
@@ -572,12 +572,17 @@ def _fetch_lending_reads(
572
572
  ),
573
573
  )
574
574
  morpho_unread: list[str] = []
575
- morpho_positions = _fetch_morpho_positions(
576
- ctx, pool, vault_addr, markets.market_substrates, morpho_unread
575
+ f_morpho = pool.submit(
576
+ _fetch_morpho_positions,
577
+ ctx,
578
+ vault_addr,
579
+ markets.market_substrates,
580
+ morpho_unread,
577
581
  )
578
582
  aave_positions = _fetch_aave_positions(
579
583
  ctx, pool, vault_addr, markets.aave_pools, markets.market_substrates
580
584
  )
585
+ morpho_positions = f_morpho.result()
581
586
  lending_health = f_health.result()
582
587
  return _LendingReads(
583
588
  lending_health=lending_health,
@@ -51,7 +51,6 @@ def _collect_morpho_substrates(
51
51
 
52
52
  def _fetch_morpho_positions(
53
53
  ctx: Web3Context,
54
- pool: ThreadPoolExecutor,
55
54
  vault_addr: ChecksumAddress,
56
55
  market_substrates: dict[MarketId, list[bytes]],
57
56
  unread: list[str],
@@ -62,35 +61,34 @@ def _fetch_morpho_positions(
62
61
  Each IPOR Morpho market (MORPHO_MARKET_IDS) can hold multiple morpho
63
62
  market_id substrates; each substrate has an independent position. The
64
63
  on-chain balance fuse reports a single netted number per IPOR market —
65
- this helper exposes the three-way decomposition behind that number.
64
+ this helper exposes the three-way decomposition behind that number. Every
65
+ substrate of every market is read in one batch; a batch failure that
66
+ ``_safe_call`` suppresses (an RPC error or timeout) leaves them all unread.
66
67
  """
67
68
  per_market_substrates = _collect_morpho_substrates(market_substrates)
68
69
  if not per_market_substrates:
69
70
  return None
70
71
 
71
72
  reader = MorphoReader(morpho_blue_address(ctx.chain_id))
72
- futures: dict[MarketId, list[Future[MorphoPositionBreakdown | None]]] = {
73
- mid: [
74
- pool.submit(
75
- _safe_call,
76
- lambda mid_hex=morpho_mid: reader.read_position_breakdown(
77
- ctx, mid_hex, vault_addr
78
- ),
79
- )
80
- for morpho_mid in morpho_mids
81
- ]
82
- for mid, morpho_mids in per_market_substrates.items()
83
- }
73
+ flat = [
74
+ morpho_mid
75
+ for morpho_mids in per_market_substrates.values()
76
+ for morpho_mid in morpho_mids
77
+ ]
78
+ reads = iter(
79
+ _safe_call(partial(reader.read_position_breakdowns, ctx, flat, vault_addr))
80
+ or [None] * len(flat)
81
+ )
84
82
 
85
83
  result: dict[MarketId, list[MorphoPositionBreakdown]] = {}
86
- for mid, per_market in futures.items():
87
- reads = [fut.result() for fut in per_market]
84
+ for mid, morpho_mids in per_market_substrates.items():
85
+ per_market = [next(reads) for _ in morpho_mids]
88
86
  unread += [
89
87
  f"Morpho position in market {morpho_mid}"
90
- for morpho_mid, bd in zip(per_market_substrates[mid], reads, strict=True)
88
+ for morpho_mid, bd in zip(morpho_mids, per_market, strict=True)
91
89
  if bd is None
92
90
  ]
93
- breakdowns = [bd for bd in reads if bd is not None]
91
+ breakdowns = [bd for bd in per_market if bd is not None]
94
92
  if breakdowns:
95
93
  result[mid] = breakdowns
96
94
  return result or None
@@ -165,8 +163,8 @@ def _fetch_aave_positions(
165
163
 
166
164
  The on-chain `getUserAccountData` aggregates all reserves into a single
167
165
  base-currency total — this helper exposes the per-asset decomposition.
168
- Each market is read from its own Pool (`aave_pools`); markets without one
169
- are skipped. Empty positions (vault allows the asset but holds none of it)
166
+ Each market is read from its own Pool (`aave_pools`), its assets in two
167
+ batches; markets without one are skipped. Empty positions (vault allows the asset but holds none of it)
170
168
  are dropped.
171
169
  """
172
170
  per_market_assets = {
@@ -177,24 +175,23 @@ def _fetch_aave_positions(
177
175
  if not per_market_assets:
178
176
  return None
179
177
 
180
- readers = {mid: AaveV3Reader(aave_pools[mid]) for mid in per_market_assets}
181
- futures: dict[MarketId, list[Future[AaveV3PositionBreakdown | None]]] = {
182
- mid: [
183
- pool.submit(
184
- _safe_call,
185
- partial(readers[mid].read_position_breakdown, ctx, asset, vault_addr),
186
- )
187
- for asset in assets
188
- ]
178
+ futures: dict[MarketId, Future[list[AaveV3PositionBreakdown | None] | None]] = {
179
+ mid: pool.submit(
180
+ _safe_call,
181
+ partial(
182
+ AaveV3Reader(aave_pools[mid]).read_position_breakdowns,
183
+ ctx,
184
+ assets,
185
+ vault_addr,
186
+ ),
187
+ )
189
188
  for mid, assets in per_market_assets.items()
190
189
  }
191
190
 
192
191
  result: dict[MarketId, list[AaveV3PositionBreakdown]] = {}
193
- for mid, per_market in futures.items():
192
+ for mid, future in futures.items():
194
193
  breakdowns = [
195
- bd
196
- for fut in per_market
197
- if (bd := fut.result()) is not None and not bd.is_empty
194
+ bd for bd in future.result() or [] if bd is not None and not bd.is_empty
198
195
  ]
199
196
  if breakdowns:
200
197
  result[mid] = breakdowns
@@ -11,10 +11,12 @@ from web3.exceptions import ContractLogicError
11
11
  # Top-level imports on purpose — they also exercise the __init__ exports.
12
12
  from ipor_fusion import (
13
13
  AccessManager,
14
+ CallPermission,
14
15
  ContractNotFoundError,
15
16
  NotPlasmaVaultError,
16
17
  OwnerNotFoundError,
17
18
  RoleAccount,
19
+ parse_access_manager_address,
18
20
  resolve_access_manager,
19
21
  role_account_sort_key,
20
22
  )
@@ -255,3 +257,47 @@ class TestResolveAccessManager:
255
257
 
256
258
  assert isinstance(manager, AccessManager)
257
259
  assert manager.address == MANAGER_ADDR
260
+
261
+
262
+ SELECTOR = bytes.fromhex("6e553f65") # deposit(uint256,address)
263
+
264
+
265
+ class TestPermissionReads:
266
+ manager = AccessManager(parse_access_manager_address(MANAGER_ADDR))
267
+
268
+ def test_can_call_encodes_and_decodes(self):
269
+ call = self.manager.can_call(ALICE, VAULT_ADDR, SELECTOR)
270
+
271
+ selector = Web3.keccak(text="canCall(address,address,bytes4)")[:4]
272
+ assert call.calldata == selector + encode(
273
+ ["address", "address", "bytes4"], [ALICE, VAULT_ADDR, SELECTOR]
274
+ )
275
+ assert call.decode(encode(["bool", "uint32"], [True, 0])) == CallPermission(
276
+ immediate=True, delay=Period(0)
277
+ )
278
+
279
+ @pytest.mark.parametrize(
280
+ ("immediate", "delay", "allowed"),
281
+ [(True, 0, True), (False, 3600, True), (False, 0, False)],
282
+ )
283
+ def test_allowed_covers_scheduled_calls(self, immediate, delay, allowed):
284
+ permission = self.manager.can_call(ALICE, VAULT_ADDR, SELECTOR).decode(
285
+ encode(["bool", "uint32"], [immediate, delay])
286
+ )
287
+
288
+ assert permission.delay == delay
289
+ assert permission.allowed is allowed
290
+
291
+ def test_is_target_closed(self):
292
+ call = self.manager.is_target_closed(VAULT_ADDR)
293
+
294
+ assert call.calldata[:4] == Web3.keccak(text="isTargetClosed(address)")[:4]
295
+ assert call.decode(encode(["bool"], [True])) is True
296
+
297
+ def test_get_account_lock_time(self):
298
+ call = self.manager.get_account_lock_time(ALICE)
299
+
300
+ assert call.calldata == Web3.keccak(text="getAccountLockTime(address)")[
301
+ :4
302
+ ] + encode(["address"], [ALICE])
303
+ assert call.decode(encode(["uint256"], [1_700_000_000])) == 1_700_000_000
@@ -1896,9 +1896,9 @@ class TestFetchAavePositions:
1896
1896
  @patch("ipor_fusion.vault_positions.AaveV3Reader")
1897
1897
  def test_drops_empty_breakdowns(self, mock_reader_cls):
1898
1898
  mock_reader = MagicMock()
1899
- mock_reader.read_position_breakdown.return_value = _aave_breakdown(
1900
- supply=0, variable_debt=0, stable_debt=0
1901
- )
1899
+ mock_reader.read_position_breakdowns.return_value = [
1900
+ _aave_breakdown(supply=0, variable_debt=0, stable_debt=0)
1901
+ ]
1902
1902
  mock_reader_cls.return_value = mock_reader
1903
1903
 
1904
1904
  with ThreadPoolExecutor() as pool:
@@ -1914,7 +1914,7 @@ class TestFetchAavePositions:
1914
1914
  @patch("ipor_fusion.vault_positions.AaveV3Reader")
1915
1915
  def test_returns_breakdowns_for_active_positions(self, mock_reader_cls):
1916
1916
  mock_reader = MagicMock()
1917
- mock_reader.read_position_breakdown.return_value = _aave_breakdown(supply=42)
1917
+ mock_reader.read_position_breakdowns.return_value = [_aave_breakdown(supply=42)]
1918
1918
  mock_reader_cls.return_value = mock_reader
1919
1919
 
1920
1920
  with ThreadPoolExecutor() as pool:
@@ -1933,8 +1933,8 @@ class TestFetchAavePositions:
1933
1933
  breakdown comes from that market's Pool, never another market's."""
1934
1934
  supply_in_pool = {pool: i for i, pool in enumerate(_AAVE_POOLS.values(), 1)}
1935
1935
  mock_reader_cls.side_effect = lambda aave_pool: MagicMock(
1936
- read_position_breakdown=MagicMock(
1937
- return_value=_aave_breakdown(supply=supply_in_pool[aave_pool])
1936
+ read_position_breakdowns=MagicMock(
1937
+ return_value=[_aave_breakdown(supply=supply_in_pool[aave_pool])]
1938
1938
  )
1939
1939
  )
1940
1940
 
@@ -1956,24 +1956,20 @@ class TestFetchAavePositions:
1956
1956
  class TestFetchMorphoPositions:
1957
1957
  @patch("ipor_fusion.vault_positions.MorphoReader")
1958
1958
  def test_returns_none_when_no_morpho_substrates(self, _mock_reader_cls):
1959
- with ThreadPoolExecutor() as pool:
1960
- result = _fetch_morpho_positions(MagicMock(), pool, _VAULT_ADDR, {}, [])
1959
+ result = _fetch_morpho_positions(MagicMock(), _VAULT_ADDR, {}, [])
1961
1960
  assert result is None
1962
1961
 
1963
1962
  @patch("ipor_fusion.vault_positions.MorphoReader")
1964
1963
  def test_returns_breakdowns_for_morpho_substrates(self, mock_reader_cls):
1965
1964
  mock_reader = MagicMock()
1966
- mock_reader.read_position_breakdown.return_value = _morpho_breakdown()
1965
+ mock_reader.read_position_breakdowns.return_value = [_morpho_breakdown()]
1967
1966
  mock_reader_cls.return_value = mock_reader
1968
1967
 
1969
1968
  ctx = MagicMock()
1970
1969
  ctx.chain_id = 42161
1971
1970
  morpho_sub = bytes.fromhex("ab" * 32)
1972
- with ThreadPoolExecutor() as pool:
1973
- unread: list[str] = []
1974
- result = _fetch_morpho_positions(
1975
- ctx, pool, _VAULT_ADDR, {14: [morpho_sub]}, unread
1976
- )
1971
+ unread: list[str] = []
1972
+ result = _fetch_morpho_positions(ctx, _VAULT_ADDR, {14: [morpho_sub]}, unread)
1977
1973
  assert result == {14: [_morpho_breakdown()]}
1978
1974
  assert unread == []
1979
1975
  # Arbitrum has its own Morpho deployment — never the Ethereum address.
@@ -1981,23 +1977,38 @@ class TestFetchMorphoPositions:
1981
1977
 
1982
1978
  @patch("ipor_fusion.vault_positions.MorphoReader")
1983
1979
  def test_a_failed_substrate_is_named_and_the_rest_kept(self, mock_reader_cls):
1984
- good, bad = bytes.fromhex("ab" * 32), bytes.fromhex("cd" * 32)
1985
-
1986
- def read(_ctx, market_id, _vault):
1987
- if market_id.endswith("cd" * 32):
1988
- raise Web3RPCError("rate limited")
1989
- return _morpho_breakdown()
1990
-
1991
- mock_reader_cls.return_value.read_position_breakdown.side_effect = read
1980
+ good, bad, other = (bytes.fromhex(h * 32) for h in ("ab", "cd", "ef"))
1981
+ mock_reader_cls.return_value.read_position_breakdowns.return_value = [
1982
+ _morpho_breakdown(),
1983
+ None,
1984
+ _morpho_breakdown(),
1985
+ ]
1992
1986
  ctx = MagicMock()
1993
1987
  ctx.chain_id = 42161
1994
1988
  unread: list[str] = []
1995
- with ThreadPoolExecutor() as pool:
1996
- result = _fetch_morpho_positions(
1997
- ctx, pool, _VAULT_ADDR, {14: [good, bad]}, unread
1998
- )
1999
- assert result == {14: [_morpho_breakdown()]}
1989
+ result = _fetch_morpho_positions(
1990
+ ctx, _VAULT_ADDR, {14: [good, bad, other]}, unread
1991
+ )
1992
+ assert result == {14: [_morpho_breakdown(), _morpho_breakdown()]}
2000
1993
  assert unread == [f"Morpho position in market 0x{'cd' * 32}"]
1994
+ # One batch for every substrate, in order.
1995
+ (_ctx, market_ids, _vault), _ = (
1996
+ mock_reader_cls.return_value.read_position_breakdowns.call_args
1997
+ )
1998
+ assert market_ids == [f"0x{h * 32}" for h in ("ab", "cd", "ef")]
1999
+
2000
+ @patch("ipor_fusion.vault_positions.MorphoReader")
2001
+ def test_a_transport_error_leaves_every_substrate_unread(self, mock_reader_cls):
2002
+ mock_reader_cls.return_value.read_position_breakdowns.side_effect = (
2003
+ Web3RPCError("rate limited")
2004
+ )
2005
+ ctx = MagicMock()
2006
+ ctx.chain_id = 42161
2007
+ unread: list[str] = []
2008
+ subs = [bytes.fromhex("ab" * 32), bytes.fromhex("cd" * 32)]
2009
+ result = _fetch_morpho_positions(ctx, _VAULT_ADDR, {14: subs}, unread)
2010
+ assert result is None
2011
+ assert unread == [f"Morpho position in market 0x{h * 32}" for h in ("ab", "cd")]
2001
2012
 
2002
2013
 
2003
2014
  class TestBreakdownTokenPrices: