ipor-fusion 4.2.0.dev11__tar.gz → 4.2.0.dev14__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 (371) hide show
  1. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/PKG-INFO +2 -2
  2. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/README.md +1 -1
  3. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/pyproject.toml +1 -1
  4. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/__init__.py +6 -0
  5. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/vault_markets_view.py +3 -3
  6. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/__init__.py +9 -1
  7. ipor_fusion-4.2.0.dev14/src/ipor_fusion/readers/compound_v3.py +164 -0
  8. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/lending_health.py +132 -55
  9. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/validation.py +8 -2
  10. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_data.py +8 -4
  11. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_positions.py +28 -1
  12. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_lending_health.py +202 -1
  13. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_vault_predates_getters.py +1 -1
  14. ipor_fusion-4.2.0.dev14/tests/test_lending_health_compound_v3_onchain.py +87 -0
  15. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_public_api.py +6 -0
  16. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_reader_encoding.py +7 -1
  17. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_validation.py +8 -0
  18. ipor_fusion-4.2.0.dev11/src/ipor_fusion/readers/compound_v3.py +0 -32
  19. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.claude-plugin/marketplace.json +0 -0
  20. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.claude-plugin/plugin.json +0 -0
  21. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.env.example +0 -0
  22. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.gemini/settings.json +0 -0
  23. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.git-blame-ignore-revs +0 -0
  24. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/cd.yml +0 -0
  25. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/ci.yml +0 -0
  26. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/commit-lint.yml +0 -0
  27. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/notify-slack.yml +0 -0
  28. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/python-build.yml +0 -0
  29. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/python-publish.yml +0 -0
  30. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/release.yml +0 -0
  31. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.github/workflows/report-slack.yml +0 -0
  32. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.gitignore +0 -0
  33. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.mcp.json +0 -0
  34. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.pre-commit-config.yaml +0 -0
  35. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/.python-version +0 -0
  36. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/AGENTS.md +0 -0
  37. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/CHANGELOG.md +0 -0
  38. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/CLAUDE.md +0 -0
  39. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/CODEOWNERS +0 -0
  40. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/LICENSE +0 -0
  41. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/MIGRATION_4.md +0 -0
  42. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/build.sh +0 -0
  43. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/CONTRIBUTING.md +0 -0
  44. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/README.md +0 -0
  45. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  46. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  47. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  48. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/external_state_margin_leg_base.py +0 -0
  49. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/hypercore_vault_official_deployment_hyperevm.py +0 -0
  50. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/examples/simple_aave_v3_supply_base.py +0 -0
  51. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/pytest.ini +0 -0
  52. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/scripts/commit-check.sh +0 -0
  53. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/scripts/fusion_error_signatures.py +0 -0
  54. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/skills/ipor-deploy-vault/SKILL.md +0 -0
  55. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/about.py +0 -0
  56. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cache.py +0 -0
  57. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/chains.py +0 -0
  58. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/__init__.py +0 -0
  59. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/address_cmd.py +0 -0
  60. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  61. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/config_cmd.py +0 -0
  62. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/config_store.py +0 -0
  63. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/fuse_cmd.py +0 -0
  64. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/main.py +0 -0
  65. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/market_cmd.py +0 -0
  66. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/options.py +0 -0
  67. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  68. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/preflight_cmd.py +0 -0
  69. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  70. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/vault_health.py +0 -0
  71. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  72. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  73. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/config/__init__.py +0 -0
  74. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/config/roles.py +0 -0
  75. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/__init__.py +0 -0
  76. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/access.py +0 -0
  77. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/address.py +0 -0
  78. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/context.py +0 -0
  79. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/context_manager.py +0 -0
  80. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/contract.py +0 -0
  81. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/erc20.py +0 -0
  82. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/external_state_executor.py +0 -0
  83. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/fee_manager.py +0 -0
  84. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/fuse.py +0 -0
  85. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/fuse_action.py +0 -0
  86. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/fuse_decoding.py +0 -0
  87. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/fusion_factory.py +0 -0
  88. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/logs.py +0 -0
  89. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/multicall.py +0 -0
  90. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/oracle.py +0 -0
  91. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/plasma_vault.py +0 -0
  92. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/rewards_manager.py +0 -0
  93. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/simulation.py +0 -0
  94. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/simulation_result.py +0 -0
  95. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  96. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/__init__.py +0 -0
  97. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  98. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  99. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  100. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  101. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  102. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  103. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  104. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  105. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  106. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/contracts.py +0 -0
  107. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/discovery.py +0 -0
  108. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/errors.py +0 -0
  109. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  110. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  111. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  112. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  113. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/lane.py +0 -0
  114. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/logs.py +0 -0
  115. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/messages.py +0 -0
  116. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/simulation.py +0 -0
  117. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  118. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  119. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  120. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  121. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  122. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/crosschain/transport.py +0 -0
  123. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/dependency_graph.py +0 -0
  124. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/entry_cli.py +0 -0
  125. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/entry_mcp.py +0 -0
  126. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/errors.py +0 -0
  127. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/explorer.py +0 -0
  128. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/field_docs.py +0 -0
  129. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuse_whitelist.py +0 -0
  130. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/__init__.py +0 -0
  131. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  132. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/async_action.py +0 -0
  133. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/base.py +0 -0
  134. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  135. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/erc4626.py +0 -0
  136. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  137. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/events.py +0 -0
  138. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/external_state.py +0 -0
  139. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/external_state_event_decoding.py +0 -0
  140. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  141. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  142. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/hypercore.py +0 -0
  143. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/hypercore_event_decoding.py +0 -0
  144. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/hypercore_pre_hooks.py +0 -0
  145. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/merkl.py +0 -0
  146. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/morpho.py +0 -0
  147. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  148. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  149. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fuses/universal.py +0 -0
  150. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/fusion_errors.py +0 -0
  151. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/guide/__init__.py +0 -0
  152. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/guide/architecture.md +0 -0
  153. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/guide/glossary.md +0 -0
  154. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/guide/invariants.md +0 -0
  155. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/guide/quickstart.md +0 -0
  156. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/hypercore/__init__.py +0 -0
  157. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/market_ids.py +0 -0
  158. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/market_info.py +0 -0
  159. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/mcp/__init__.py +0 -0
  160. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/mcp/guide.py +0 -0
  161. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/mcp/models.py +0 -0
  162. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/mcp/server.py +0 -0
  163. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/morpho_api.py +0 -0
  164. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/onchain_text.py +0 -0
  165. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/preflight.py +0 -0
  166. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/py.typed +0 -0
  167. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/aave_v3.py +0 -0
  168. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/aave_v4.py +0 -0
  169. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/euler_v2.py +0 -0
  170. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/hypercore.py +0 -0
  171. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  172. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/morpho.py +0 -0
  173. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  174. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  175. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  176. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/position_manager.py +0 -0
  177. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  178. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  179. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/readers/unpriceable.py +0 -0
  180. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/registry.py +0 -0
  181. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/substrates.py +0 -0
  182. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/types.py +0 -0
  183. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_erc20.py +0 -0
  184. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_info.py +0 -0
  185. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_managers.py +0 -0
  186. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/src/ipor_fusion/vault_reads.py +0 -0
  187. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/UniswapV3UniversalRouter.py +0 -0
  188. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_addresses.py +0 -0
  189. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_crosschain.py +0 -0
  190. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_crosschain_lifecycle.py +0 -0
  191. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_crosschain_recovery.py +0 -0
  192. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_euler_v2.py +0 -0
  193. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_event_args.py +0 -0
  194. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_fake_rpc.py +0 -0
  195. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_foundry.py +0 -0
  196. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_golden.py +0 -0
  197. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_guide.py +0 -0
  198. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_hypercore_model.py +0 -0
  199. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_hypercore_shadow.py +0 -0
  200. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_live.py +0 -0
  201. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_multicall.py +0 -0
  202. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_oracle_mapping_fake.py +0 -0
  203. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_simulate.py +0 -0
  204. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/_validation.py +0 -0
  205. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/addresses.py +0 -0
  206. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/conftest.py +0 -0
  207. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/constants.py +0 -0
  208. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  209. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/hypercore_market55.json +0 -0
  210. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/hypercore_official.json +0 -0
  211. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/meta_morpho_v1.json +0 -0
  212. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/meta_morpho_v1.mcp.json +0 -0
  213. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/meta_morpho_v2.json +0 -0
  214. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/meta_morpho_v2.mcp.json +0 -0
  215. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_api/market.json +0 -0
  216. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_api/vault_v1.json +0 -0
  217. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_api/vault_v2.json +0 -0
  218. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_api/vault_v2_not_found.json +0 -0
  219. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue.json +0 -0
  220. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue.mcp.json +0 -0
  221. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue_api_error.json +0 -0
  222. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue_api_error.mcp.json +0 -0
  223. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue_no_api.json +0 -0
  224. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/market_golden/morpho_blue_no_api.mcp.json +0 -0
  225. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/oracle_mapping_golden/mapping.json +0 -0
  226. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/oracle_mapping_golden/mapping.mcp.json +0 -0
  227. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/simulate_v1_request.json +0 -0
  228. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/clean.json +0 -0
  229. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/clean.mcp.json +0 -0
  230. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  231. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/deployment_error.json +0 -0
  232. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/deployment_error.mcp.json +0 -0
  233. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/deployment_error.txt +0 -0
  234. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/fetched.cli.json +0 -0
  235. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/fetched.json +0 -0
  236. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/fetched.mcp.json +0 -0
  237. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/full.json +0 -0
  238. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/full.mcp.json +0 -0
  239. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/full.txt +0 -0
  240. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/rich.json +0 -0
  241. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/rich.mcp.json +0 -0
  242. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  243. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_about.py +0 -0
  244. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_access_manager.py +0 -0
  245. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_addresses_helper.py +0 -0
  246. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cache.py +0 -0
  247. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_ccip_read_disabled.py +0 -0
  248. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_chains.py +0 -0
  249. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_address.py +0 -0
  250. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_changelog.py +0 -0
  251. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_commands.py +0 -0
  252. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_config_store.py +0 -0
  253. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_helpers.py +0 -0
  254. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_hypercore.py +0 -0
  255. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_market_golden.py +0 -0
  256. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_market_morpho_blue.py +0 -0
  257. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_mcp_fuse_whitelist.py +0 -0
  258. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_mcp_parity.py +0 -0
  259. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_oracle_mapping.py +0 -0
  260. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_oracle_mapping_golden.py +0 -0
  261. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_pending_requests_scan.py +0 -0
  262. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_preflight.py +0 -0
  263. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_role_accounts.py +0 -0
  264. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_vault_info_golden.py +0 -0
  265. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_cli_vault_validate.py +0 -0
  266. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_contract_call.py +0 -0
  267. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_contract_encoder.py +0 -0
  268. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_ccip_events.py +0 -0
  269. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_ccip_indexer.py +0 -0
  270. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_ccip_quote.py +0 -0
  271. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_chainlink.py +0 -0
  272. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_contracts.py +0 -0
  273. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_encoding.py +0 -0
  274. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_errors.py +0 -0
  275. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_lane.py +0 -0
  276. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_layering.py +0 -0
  277. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_crosschain_transport.py +0 -0
  278. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_docs_typing.py +0 -0
  279. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_erc20.py +0 -0
  280. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_error_hierarchy.py +0 -0
  281. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_examples_crosschain.py +0 -0
  282. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  283. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_examples_hypercore_hyperevm.py +0 -0
  284. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_examples_vaults.py +0 -0
  285. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_explorer.py +0 -0
  286. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_external_state_events.py +0 -0
  287. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_external_state_executor.py +0 -0
  288. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fee_manager.py +0 -0
  289. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fuse_decoding.py +0 -0
  290. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fuse_encoding.py +0 -0
  291. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fuse_whitelist.py +0 -0
  292. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fusion_errors.py +0 -0
  293. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_fusion_instance.py +0 -0
  294. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_governance_wrappers.py +0 -0
  295. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_guide.py +0 -0
  296. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_deployment.py +0 -0
  297. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_events.py +0 -0
  298. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_flow.py +0 -0
  299. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_live.py +0 -0
  300. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_outcome.py +0 -0
  301. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_pre_hooks.py +0 -0
  302. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_readers.py +0 -0
  303. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_hypercore_simulation.py +0 -0
  304. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_logs.py +0 -0
  305. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_address.py +0 -0
  306. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_hypercore.py +0 -0
  307. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_models.py +0 -0
  308. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_preflight.py +0 -0
  309. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_server.py +0 -0
  310. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_mcp_vault_validate.py +0 -0
  311. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_multicall.py +0 -0
  312. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_onchain_text.py +0 -0
  313. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_oracle.py +0 -0
  314. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_oracle_mapping.py +0 -0
  315. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_plasma_vault.py +0 -0
  316. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_preflight.py +0 -0
  317. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_public_type_reachability.py +0 -0
  318. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_registry.py +0 -0
  319. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_revert_decoding.py +0 -0
  320. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_rewards_manager.py +0 -0
  321. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_roles.py +0 -0
  322. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_sdk_layering.py +0 -0
  323. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  324. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  325. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  326. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_compound_v3_institutional_usdc.py +0 -0
  327. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  328. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  329. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  330. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  331. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  332. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  333. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_examples_arbitrum.py +0 -0
  334. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_examples_base.py +0 -0
  335. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_examples_crosschain.py +0 -0
  336. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_examples_hyperevm.py +0 -0
  337. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_external_state_executor_base.py +0 -0
  338. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  339. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  340. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  341. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_morpho_blue.py +0 -0
  342. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_pilot_v3.py +0 -0
  343. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  344. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  345. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  346. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  347. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_preflight_base.py +0 -0
  348. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_readers_integration.py +0 -0
  349. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_release_funds.py +0 -0
  350. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_swap_on_base.py +0 -0
  351. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_timelock_base.py +0 -0
  352. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  353. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulation_errors.py +0 -0
  354. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_simulation_observation.py +0 -0
  355. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_solidity_compiler.py +0 -0
  356. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_solidity_mirrors.py +0 -0
  357. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_substrates.py +0 -0
  358. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_types_and_events.py +0 -0
  359. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_unpriceable_tokens.py +0 -0
  360. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_data_block.py +0 -0
  361. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_dep_graph.py +0 -0
  362. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_info_fetched_golden.py +0 -0
  363. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_info_onchain_base.py +0 -0
  364. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  365. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_reconciliation_onchain_base.py +0 -0
  366. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_vault_simulator_blocks.py +0 -0
  367. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_web3_context.py +0 -0
  368. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/test_withdraw_manager.py +0 -0
  369. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/typed_client/consumer.py +0 -0
  370. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/tests/typed_client/rejected.py +0 -0
  371. {ipor_fusion-4.2.0.dev11 → ipor_fusion-4.2.0.dev14}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.2.0.dev11
3
+ Version: 4.2.0.dev14
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
@@ -564,7 +564,7 @@ Read-only aggregators over positions and health, no fuses involved:
564
564
  |--------|---------|
565
565
  | `MorphoReader`, `AaveV3Reader`, `CompoundV3Reader` | Market params, rates and vault positions per lending protocol |
566
566
  | `UniswapV3Reader`, `RamsesV2Reader` | LP position details |
567
- | `fetch_vault_lending_health` | Health factor per lending market a vault is in (Morpho, Aave V3/V4, Euler V2; any other market that can borrow is listed as unreadable) |
567
+ | `fetch_vault_lending_health` | Health factor per lending market a vault is in (Morpho, Aave V3/V4, Euler V2, Compound V3; any other market that can borrow is listed as unreadable) |
568
568
  | `build_oracle_mapping` | How the vault prices every configured asset |
569
569
 
570
570
  ### Supported networks
@@ -515,7 +515,7 @@ Read-only aggregators over positions and health, no fuses involved:
515
515
  |--------|---------|
516
516
  | `MorphoReader`, `AaveV3Reader`, `CompoundV3Reader` | Market params, rates and vault positions per lending protocol |
517
517
  | `UniswapV3Reader`, `RamsesV2Reader` | LP position details |
518
- | `fetch_vault_lending_health` | Health factor per lending market a vault is in (Morpho, Aave V3/V4, Euler V2; any other market that can borrow is listed as unreadable) |
518
+ | `fetch_vault_lending_health` | Health factor per lending market a vault is in (Morpho, Aave V3/V4, Euler V2, Compound V3; any other market that can borrow is listed as unreadable) |
519
519
  | `build_oracle_mapping` | How the vault prices every configured asset |
520
520
 
521
521
  ### Supported networks
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.2.0.dev11"
3
+ version = "4.2.0.dev14"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -312,6 +312,9 @@ from ipor_fusion.readers import (
312
312
  AaveV4OracleReader,
313
313
  AaveV4SpokeReader,
314
314
  AaveV4UserAccountData,
315
+ CometAccountValue,
316
+ CometAssetInfo,
317
+ CompoundV3FuseReader,
315
318
  CompoundV3Reader,
316
319
  LendingMarketHealth,
317
320
  MiddlewarePricedToken,
@@ -788,6 +791,9 @@ __all__ = [
788
791
  "AaveV4OracleReader",
789
792
  "AaveV4SpokeReader",
790
793
  "AaveV4UserAccountData",
794
+ "CometAccountValue",
795
+ "CometAssetInfo",
796
+ "CompoundV3FuseReader",
791
797
  "CompoundV3Reader",
792
798
  "HYPEREVM_CHAIN_ID",
793
799
  "LendingMarketHealth",
@@ -116,8 +116,8 @@ def _index_lending_health(
116
116
  """Split lending health rows by protocol for fast lookup during rendering.
117
117
 
118
118
  Morpho rows are keyed by morpho substrate id (one row per substrate).
119
- Aave V3 rows are keyed by IPOR market id (account-aggregated, one row per
120
- market); Aave V4 rows by Spoke address (account-aggregated per Spoke) and
119
+ Aave V3 and Compound V3 rows are keyed by IPOR market id (account-aggregated,
120
+ one row per market); Aave V4 rows by Spoke address (account-aggregated per Spoke) and
121
121
  Euler V2 rows by sub-account address, in the same account-level map.
122
122
  """
123
123
  morpho: dict[str, LendingMarketHealth] = {}
@@ -127,7 +127,7 @@ def _index_lending_health(
127
127
  for m in lh.markets:
128
128
  if m.protocol == "morpho" and m.substrate_id:
129
129
  morpho[str(m.substrate_id).lower().removeprefix("0x")] = m
130
- elif m.protocol == "aave_v3":
130
+ elif m.protocol in ("aave_v3", "compound_v3"):
131
131
  aave[m.market_id] = m
132
132
  elif m.protocol in ("aave_v4", "euler_v2") and m.substrate_id:
133
133
  aave[m.substrate_id] = m
@@ -11,7 +11,12 @@ from ipor_fusion.readers.aave_v4 import (
11
11
  AaveV4SpokeReader,
12
12
  AaveV4UserAccountData,
13
13
  )
14
- from ipor_fusion.readers.compound_v3 import CompoundV3Reader
14
+ from ipor_fusion.readers.compound_v3 import (
15
+ CometAccountValue,
16
+ CometAssetInfo,
17
+ CompoundV3FuseReader,
18
+ CompoundV3Reader,
19
+ )
15
20
  from ipor_fusion.readers.hypercore import (
16
21
  HYPEREVM_CHAIN_ID,
17
22
  )
@@ -79,6 +84,9 @@ __all__ = [
79
84
  "AaveV4OracleReader",
80
85
  "AaveV4SpokeReader",
81
86
  "AaveV4UserAccountData",
87
+ "CometAccountValue",
88
+ "CometAssetInfo",
89
+ "CompoundV3FuseReader",
82
90
  "CompoundV3Reader",
83
91
  "HYPEREVM_CHAIN_ID",
84
92
  "LendingMarketHealth",
@@ -0,0 +1,164 @@
1
+ from dataclasses import dataclass
2
+
3
+ from eth_typing import ChecksumAddress
4
+ from web3 import Web3
5
+
6
+ from ipor_fusion.core.context import Web3Context
7
+ from ipor_fusion.core.contract import Call, ContractWrapper
8
+ from ipor_fusion.core.multicall import Multicall3
9
+ from ipor_fusion.types import Amount
10
+
11
+ COMET_FACTOR_SCALE = 10**18
12
+ COMET_PRICE_SCALE = 10**8
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CometAssetInfo:
17
+ """A collateral listing of a Comet; the factors are scaled by 1e18."""
18
+
19
+ asset: ChecksumAddress
20
+ price_feed: ChecksumAddress
21
+ scale: int
22
+ borrow_collateral_factor: int
23
+ liquidate_collateral_factor: int
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class CometAccountValue:
28
+ """An account's position in the Comet's own prices, scaled by 1e8.
29
+
30
+ The unit is the Comet's price unit: USD on USD-priced Comets, ETH on a WETH
31
+ Comet. ``liquidation_limit`` is the collateral at its liquidation factors;
32
+ the account is liquidatable when ``debt`` exceeds it.
33
+ """
34
+
35
+ collateral: int
36
+ liquidation_limit: int
37
+ debt: int
38
+
39
+
40
+ def _asset_info_decoder(
41
+ value: tuple[int, str, str, int, int, int, int, int],
42
+ ) -> CometAssetInfo:
43
+ _, asset, price_feed, scale, borrow_factor, liquidate_factor, _, _ = value
44
+ return CometAssetInfo(
45
+ asset=Web3.to_checksum_address(asset),
46
+ price_feed=Web3.to_checksum_address(price_feed),
47
+ scale=scale,
48
+ borrow_collateral_factor=borrow_factor,
49
+ liquidate_collateral_factor=liquidate_factor,
50
+ )
51
+
52
+
53
+ class CompoundV3FuseReader(ContractWrapper):
54
+ """Reader for the immutables of an IPOR Fusion Compound V3 fuse.
55
+
56
+ A Fusion market is bound to one Comet per chain through its fuses, so the
57
+ Comet behind a market is a property of its fuse.
58
+ """
59
+
60
+ def comet(self) -> Call[ChecksumAddress]:
61
+ return self._view(
62
+ "COMET()", output_types=["address"], decoder=Web3.to_checksum_address
63
+ )
64
+
65
+
66
+ class CompoundV3Reader(ContractWrapper):
67
+ """Reader for Compound V3 (Comet) on-chain state."""
68
+
69
+ def balance_of(self, account: ChecksumAddress) -> Call[Amount]:
70
+ return self._view(
71
+ "balanceOf(address)", account, output_types=["uint256"], decoder=Amount
72
+ )
73
+
74
+ def borrow_balance_of(self, account: ChecksumAddress) -> Call[Amount]:
75
+ return self._view(
76
+ "borrowBalanceOf(address)",
77
+ account,
78
+ output_types=["uint256"],
79
+ decoder=Amount,
80
+ )
81
+
82
+ def collateral_balance_of(
83
+ self, account: ChecksumAddress, asset: ChecksumAddress
84
+ ) -> Call[Amount]:
85
+ return self._view(
86
+ "collateralBalanceOf(address,address)",
87
+ account,
88
+ asset,
89
+ output_types=["uint128"],
90
+ decoder=Amount,
91
+ )
92
+
93
+ def base_scale(self) -> Call[int]:
94
+ return self._view("baseScale()", output_types=["uint256"])
95
+
96
+ def base_token_price_feed(self) -> Call[ChecksumAddress]:
97
+ return self._view(
98
+ "baseTokenPriceFeed()",
99
+ output_types=["address"],
100
+ decoder=Web3.to_checksum_address,
101
+ )
102
+
103
+ def get_price(self, price_feed: ChecksumAddress) -> Call[int]:
104
+ return self._view("getPrice(address)", price_feed, output_types=["uint256"])
105
+
106
+ def num_assets(self) -> Call[int]:
107
+ return self._view("numAssets()", output_types=["uint8"])
108
+
109
+ def get_asset_info(self, index: int) -> Call[CometAssetInfo]:
110
+ return self._view(
111
+ "getAssetInfo(uint8)",
112
+ index,
113
+ output_types=[
114
+ "(uint8,address,address,uint64,uint64,uint64,uint64,uint128)"
115
+ ],
116
+ decoder=_asset_info_decoder,
117
+ )
118
+
119
+ def read_account_value(
120
+ self, ctx: Web3Context, account: ChecksumAddress
121
+ ) -> CometAccountValue:
122
+ """Value ``account``'s collateral and debt as the Comet does, over every
123
+ collateral it lists rather than the vault's substrates: a revoked substrate
124
+ does not take collateral out of the account. Only the feeds of held assets
125
+ are read, as `isLiquidatable` does, so a broken feed of an asset the account
126
+ does not hold cannot hide its position (four batches)."""
127
+ multicall = Multicall3(ctx)
128
+ batch = multicall.batch()
129
+ count = batch.add(self.num_assets())
130
+ base_scale = batch.add(self.base_scale())
131
+ base_feed = batch.add(self.base_token_price_feed())
132
+ debt = batch.add(self.borrow_balance_of(account))
133
+ batch.run()
134
+
135
+ infos = multicall.aggregate(
136
+ [self.get_asset_info(i) for i in range(count.value)]
137
+ )
138
+ balances = multicall.aggregate(
139
+ [self.collateral_balance_of(account, info.asset) for info in infos]
140
+ )
141
+ held = [
142
+ (info, balance)
143
+ for info, balance in zip(infos, balances, strict=True)
144
+ if balance > 0
145
+ ]
146
+
147
+ batch = multicall.batch()
148
+ base_price = batch.add(self.get_price(base_feed.value))
149
+ prices = [batch.add(self.get_price(info.price_feed)) for info, _ in held]
150
+ batch.run()
151
+
152
+ collateral = 0
153
+ liquidation_limit = 0
154
+ for (info, balance), price in zip(held, prices, strict=True):
155
+ value = balance * price.value // info.scale
156
+ collateral += value
157
+ liquidation_limit += (
158
+ value * info.liquidate_collateral_factor // COMET_FACTOR_SCALE
159
+ )
160
+ return CometAccountValue(
161
+ collateral=collateral,
162
+ liquidation_limit=liquidation_limit,
163
+ debt=debt.value * base_price.value // base_scale.value,
164
+ )
@@ -1,4 +1,5 @@
1
- """Lending health computation for Morpho Blue, Aave V3, Aave V4 and Euler V2 markets.
1
+ """Lending health computation for Morpho Blue, Aave V3, Aave V4, Euler V2 and
2
+ Compound V3 markets.
2
3
 
3
4
  Provides a unified view of LTV, liquidation thresholds, and health factors
4
5
  for lending positions held by Plasma Vaults.
@@ -7,8 +8,10 @@ for lending positions held by Plasma Vaults.
7
8
  from __future__ import annotations
8
9
 
9
10
  import logging
11
+ from collections.abc import Callable
10
12
  from concurrent.futures import ThreadPoolExecutor
11
13
  from dataclasses import dataclass, field
14
+ from functools import partial
12
15
 
13
16
  import requests
14
17
  from eth_abi import decode
@@ -30,6 +33,7 @@ from ipor_fusion.errors import EmptyCallResultError
30
33
  from ipor_fusion.market_ids import IporFusionMarkets
31
34
  from ipor_fusion.readers.aave_v3 import AaveV3Reader
32
35
  from ipor_fusion.readers.aave_v4 import AaveV4OracleReader, AaveV4SpokeReader
36
+ from ipor_fusion.readers.compound_v3 import COMET_PRICE_SCALE, CompoundV3Reader
33
37
  from ipor_fusion.readers.euler_v2 import (
34
38
  EULER_USD_UNIT_OF_ACCOUNT,
35
39
  EulerV2Substrate,
@@ -123,6 +127,18 @@ EULER_V2_MARKET_IDS = frozenset(
123
127
  }
124
128
  )
125
129
 
130
+ # Market IDs served by the Compound V3 fuses. One Comet per market, read from its
131
+ # balance fuse. Health is account-level; a Comet account owing nothing has no
132
+ # liquidation risk and no row. The WETH Comet prices in ETH, not USD.
133
+ COMPOUND_V3_MARKET_IDS = frozenset(
134
+ {
135
+ IporFusionMarkets.COMPOUND_V3_USDC,
136
+ IporFusionMarkets.COMPOUND_V3_USDT,
137
+ IporFusionMarkets.COMPOUND_V3_WETH,
138
+ IporFusionMarkets.COMPOUND_V3_INSTITUTIONAL_USDC,
139
+ }
140
+ )
141
+
126
142
  # Markets whose fuses can borrow but whose health has no reader yet. Their
127
143
  # positions are listed as unreadable rather than left out: an unknown
128
144
  # liquidation risk is not a healthy one. Dolomite counts only when a
@@ -132,7 +148,6 @@ UNSUPPORTED_BORROW_MARKET_IDS = frozenset(
132
148
  IporFusionMarkets.MOONWELL,
133
149
  IporFusionMarkets.SILO_V2,
134
150
  IporFusionMarkets.EBISU,
135
- IporFusionMarkets.COMPOUND_V3_INSTITUTIONAL_USDC,
136
151
  }
137
152
  )
138
153
  _UNSUPPORTED_REASON = "no lending health reader for this market"
@@ -147,6 +162,7 @@ class LendingMarketHealth:
147
162
  - Aave V3: None — Aave health is account-level, aggregated across reserves.
148
163
  - Aave V4: the Spoke address — account-level per Spoke, across its reserves.
149
164
  - Euler V2: the sub-account address — account-level, across its collaterals.
165
+ - Compound V3: None — account-level on the market's Comet, across its collaterals.
150
166
  """
151
167
 
152
168
  protocol: str
@@ -438,6 +454,57 @@ def _compute_aave_v4_market_health(
438
454
  )
439
455
 
440
456
 
457
+ def _compute_compound_v3_market_health(
458
+ ctx: Web3Context,
459
+ comet: ChecksumAddress,
460
+ vault_address: PlasmaVaultAddress,
461
+ ipor_market_id: MarketId,
462
+ market_name: str,
463
+ ) -> LendingMarketHealth | UnreadableLendingMarket | None:
464
+ """Compute LTV health for the vault's account on one Comet; None when it owes
465
+ nothing. The health factor is the liquidation limit over the debt, so it falls
466
+ to 1 where `Comet.isLiquidatable` turns true."""
467
+ try:
468
+ reader = CompoundV3Reader(comet)
469
+ if reader.borrow_balance_of(vault_address).call(ctx) == 0:
470
+ return None
471
+ account = reader.read_account_value(ctx, vault_address)
472
+ except _READ_ERRORS as exc:
473
+ return UnreadableLendingMarket(
474
+ protocol="compound_v3",
475
+ market_id=ipor_market_id,
476
+ market_name=market_name,
477
+ reason=_reason("account value", exc),
478
+ )
479
+ # Dust that prices to nothing: the Comet rounds it the same way and cannot
480
+ # liquidate it.
481
+ if account.debt == 0:
482
+ return None
483
+
484
+ has_collateral = account.collateral > 0
485
+ has_limit = account.liquidation_limit > 0
486
+ current_ltv = account.debt / account.collateral if has_collateral else None
487
+ max_ltv = account.liquidation_limit / account.collateral if has_collateral else 0.0
488
+ # With no limit the health factor is 0, a known liquidation risk; only the usage
489
+ # has no value.
490
+ ltv_usage = account.debt / account.liquidation_limit * 100 if has_limit else None
491
+ in_usd = ipor_market_id != IporFusionMarkets.COMPOUND_V3_WETH
492
+
493
+ return LendingMarketHealth(
494
+ protocol="compound_v3",
495
+ market_id=ipor_market_id,
496
+ market_name=market_name,
497
+ current_ltv=round(current_ltv, 6) if current_ltv is not None else None,
498
+ max_ltv=round(max_ltv, 6),
499
+ health_factor=round(account.liquidation_limit / account.debt, 4),
500
+ total_collateral_usd=(
501
+ round(account.collateral / COMET_PRICE_SCALE, 2) if in_usd else None
502
+ ),
503
+ total_debt_usd=round(account.debt / COMET_PRICE_SCALE, 2) if in_usd else None,
504
+ ltv_usage_percent=round(ltv_usage, 2) if ltv_usage is not None else None,
505
+ )
506
+
507
+
441
508
  @dataclass(frozen=True, slots=True)
442
509
  class _EulerV2Account:
443
510
  """An EVC sub-account of the vault and the Euler vaults it may borrow from."""
@@ -575,6 +642,7 @@ class _LendingMarkets:
575
642
  aave_v3: list[tuple[MarketId, str, ChecksumAddress]]
576
643
  aave_v4: list[tuple[MarketId, str, ChecksumAddress]]
577
644
  euler_v2: list[tuple[MarketId, str, _EulerV2Account]]
645
+ compound_v3: list[tuple[MarketId, str, ChecksumAddress]]
578
646
  unreadable: list[UnreadableLendingMarket]
579
647
 
580
648
 
@@ -583,12 +651,14 @@ def _collect_lending_markets(
583
651
  balance_fuse_market_ids: list[MarketId],
584
652
  market_substrates: dict[MarketId, list[bytes]],
585
653
  aave_pools: dict[MarketId, ChecksumAddress],
654
+ comets: dict[MarketId, ChecksumAddress],
586
655
  ) -> _LendingMarkets:
587
656
  """Pick the positions to read: Morpho substrates, Aave V3 markets with a Pool,
588
- the Spokes of Aave V4 markets and the borrowing Euler V2 sub-accounts. A
589
- market that may borrow but has no reader is listed as unreadable."""
657
+ the Spokes of Aave V4 markets, the borrowing Euler V2 sub-accounts and Compound
658
+ V3 markets with a Comet. A market that may borrow but has no reader is listed
659
+ as unreadable."""
590
660
  markets = _LendingMarkets(
591
- morpho=[], aave_v3=[], aave_v4=[], euler_v2=[], unreadable=[]
661
+ morpho=[], aave_v3=[], aave_v4=[], euler_v2=[], compound_v3=[], unreadable=[]
592
662
  )
593
663
 
594
664
  for mid in balance_fuse_market_ids:
@@ -621,6 +691,18 @@ def _collect_lending_markets(
621
691
  (mid, name, account)
622
692
  for account in _euler_v2_borrow_accounts(substrates, vault)
623
693
  )
694
+ elif mid in COMPOUND_V3_MARKET_IDS:
695
+ if comet := comets.get(mid):
696
+ markets.compound_v3.append((mid, name, comet))
697
+ else:
698
+ markets.unreadable.append(
699
+ UnreadableLendingMarket(
700
+ protocol="compound_v3",
701
+ market_id=mid,
702
+ market_name=name,
703
+ reason="the balance fuse names no Comet",
704
+ )
705
+ )
624
706
  elif _can_borrow(mid, substrates):
625
707
  markets.unreadable.append(
626
708
  UnreadableLendingMarket(
@@ -634,6 +716,45 @@ def _collect_lending_markets(
634
716
  return markets
635
717
 
636
718
 
719
+ def _health_reads(
720
+ ctx: Web3Context, vault: PlasmaVaultAddress, markets: _LendingMarkets
721
+ ) -> list[Callable[[], LendingMarketHealth | UnreadableLendingMarket | None]]:
722
+ """One read per position; Aave V3 health is account-level per Pool, so each
723
+ market reads its own."""
724
+ reads: list[Callable[[], LendingMarketHealth | UnreadableLendingMarket | None]] = []
725
+ if markets.morpho:
726
+ morpho_reader = MorphoReader(morpho_blue_address(ctx.chain_id))
727
+ reads += [
728
+ partial(
729
+ _compute_morpho_market_health,
730
+ ctx,
731
+ morpho_reader,
732
+ morpho_mid,
733
+ vault,
734
+ mid,
735
+ name,
736
+ )
737
+ for mid, name, morpho_mid in markets.morpho
738
+ ]
739
+ reads += [
740
+ partial(_compute_aave_market_health, ctx, AaveV3Reader(pool), vault, mid, name)
741
+ for mid, name, pool in markets.aave_v3
742
+ ]
743
+ reads += [
744
+ partial(_compute_aave_v4_market_health, ctx, spoke, vault, mid, name)
745
+ for mid, name, spoke in markets.aave_v4
746
+ ]
747
+ reads += [
748
+ partial(_compute_euler_v2_account_health, ctx, account, mid, name)
749
+ for mid, name, account in markets.euler_v2
750
+ ]
751
+ reads += [
752
+ partial(_compute_compound_v3_market_health, ctx, comet, vault, mid, name)
753
+ for mid, name, comet in markets.compound_v3
754
+ ]
755
+ return reads
756
+
757
+
637
758
  def fetch_vault_lending_health(
638
759
  ctx: Web3Context,
639
760
  vault: PlasmaVaultAddress,
@@ -641,6 +762,7 @@ def fetch_vault_lending_health(
641
762
  balance_fuse_market_ids: list[MarketId],
642
763
  market_substrates: dict[MarketId, list[bytes]],
643
764
  aave_pools: dict[MarketId, ChecksumAddress] | None = None,
765
+ comets: dict[MarketId, ChecksumAddress] | None = None,
644
766
  ) -> VaultLendingHealth:
645
767
  """Fetch lending health for all lending markets in a vault.
646
768
 
@@ -656,68 +778,23 @@ def fetch_vault_lending_health(
656
778
  entry is listed in ``unreadable``. Defaults to the chain's Core pool for AAVE_V3 only:
657
779
  the Pools of Aave V3 Prime and SparkLend are known only to their
658
780
  fuses.
781
+ comets: Map of Compound V3 market_id -> the Comet its balance fuse reads
782
+ (`CompoundV3FuseReader.comet`). A Compound V3 market without an entry
783
+ is listed in ``unreadable``.
659
784
  """
660
785
  if aave_pools is None:
661
786
  core_pool = AAVE_V3_POOL.get(ctx.chain_id)
662
787
  aave_pools = {IporFusionMarkets.AAVE_V3: core_pool} if core_pool else {}
663
788
 
664
789
  markets = _collect_lending_markets(
665
- vault, balance_fuse_market_ids, market_substrates, aave_pools
790
+ vault, balance_fuse_market_ids, market_substrates, aave_pools, comets or {}
666
791
  )
667
792
 
668
793
  results: list[LendingMarketHealth] = []
669
794
  unreadable = list(markets.unreadable)
670
795
 
671
796
  with ThreadPoolExecutor() as pool:
672
- futures = []
673
-
674
- if markets.morpho:
675
- morpho_reader = MorphoReader(morpho_blue_address(ctx.chain_id))
676
- for ipor_mid, name, morpho_mid in markets.morpho:
677
- futures.append(
678
- pool.submit(
679
- _compute_morpho_market_health,
680
- ctx,
681
- morpho_reader,
682
- morpho_mid,
683
- vault,
684
- ipor_mid,
685
- name,
686
- )
687
- )
688
-
689
- # Aave V3 health is account-level per Pool, so each market reads its own.
690
- for ipor_mid, name, aave_pool in markets.aave_v3:
691
- futures.append(
692
- pool.submit(
693
- _compute_aave_market_health,
694
- ctx,
695
- AaveV3Reader(aave_pool),
696
- vault,
697
- ipor_mid,
698
- name,
699
- )
700
- )
701
-
702
- for ipor_mid, name, spoke in markets.aave_v4:
703
- futures.append(
704
- pool.submit(
705
- _compute_aave_v4_market_health,
706
- ctx,
707
- spoke,
708
- vault,
709
- ipor_mid,
710
- name,
711
- )
712
- )
713
-
714
- for ipor_mid, name, account in markets.euler_v2:
715
- futures.append(
716
- pool.submit(
717
- _compute_euler_v2_account_health, ctx, account, ipor_mid, name
718
- )
719
- )
720
-
797
+ futures = [pool.submit(read) for read in _health_reads(ctx, vault, markets)]
721
798
  for fut in futures:
722
799
  result = fut.result()
723
800
  if result is None:
@@ -691,9 +691,15 @@ def _lending_health_findings(state: VaultState) -> list[Finding]:
691
691
  for u in health.unreadable
692
692
  ]
693
693
  for market in health.markets:
694
- if market.ltv_usage_percent is None:
694
+ # A critical row with no usage (debt against a zero limit) is still critical.
695
+ if market.ltv_usage_percent is None and not market.is_critical:
695
696
  continue
696
697
  ltv = f"{market.current_ltv:.4f}" if market.current_ltv is not None else "?"
698
+ usage = (
699
+ f"{market.ltv_usage_percent:.1f}%"
700
+ if market.ltv_usage_percent is not None
701
+ else "?"
702
+ )
697
703
  health_factor = (
698
704
  f", health_factor={market.health_factor:.4f}"
699
705
  if market.health_factor is not None
@@ -702,7 +708,7 @@ def _lending_health_findings(state: VaultState) -> list[Finding]:
702
708
  line = (
703
709
  f"{market.protocol} {market.market_name}: "
704
710
  f"LTV {ltv}/{market.max_ltv:.4f} "
705
- f"({market.ltv_usage_percent:.1f}% usage){health_factor}"
711
+ f"({usage} usage){health_factor}"
706
712
  )
707
713
  if market.is_critical:
708
714
  findings.append(
@@ -88,6 +88,7 @@ from ipor_fusion.vault_positions import (
88
88
  _fetch_aave_pools,
89
89
  _fetch_aave_positions,
90
90
  _fetch_breakdown_token_prices,
91
+ _fetch_comets,
91
92
  _fetch_morpho_positions,
92
93
  )
93
94
  from ipor_fusion.vault_reads import (
@@ -508,16 +509,17 @@ class _MarketReads:
508
509
  # Only markets with at least one substrate.
509
510
  market_substrates: dict[MarketId, list[bytes]]
510
511
  aave_pools: dict[MarketId, ChecksumAddress]
512
+ comets: dict[MarketId, ChecksumAddress]
511
513
  market_balances: dict[MarketId, Amount]
512
514
  # False when the vault predates getMarketSubstrates (substrates unknown).
513
515
  substrates_available: bool = True
514
516
 
515
517
 
516
518
  def _fetch_market_reads(
517
- ctx: Web3Context, plasma_vault: PlasmaVault, *, with_aave_pools: bool
519
+ ctx: Web3Context, plasma_vault: PlasmaVault, *, with_lending_venues: bool
518
520
  ) -> _MarketReads:
519
521
  """Balance fuses, then every market's dependency graph and substrates in
520
- one batch, then (optionally) the Aave V3 Pools behind the markets."""
522
+ one batch, then (optionally) the Aave V3 Pools and Comets behind the markets."""
521
523
  balance_fuses = plasma_vault.read_balance_fuses(ctx)
522
524
  market_ids = [bf.market_id for bf in balance_fuses]
523
525
  venues = list(dict.fromkeys(market_ids))
@@ -545,7 +547,8 @@ def _fetch_market_reads(
545
547
  market_substrates={
546
548
  mid: subs for mid, subs in zip(market_ids, substrates, strict=True) if subs
547
549
  },
548
- aave_pools=_fetch_aave_pools(ctx, balance_fuses) if with_aave_pools else {},
550
+ aave_pools=_fetch_aave_pools(ctx, balance_fuses) if with_lending_venues else {},
551
+ comets=_fetch_comets(ctx, balance_fuses) if with_lending_venues else {},
549
552
  market_balances={mid: balance.value for mid, balance in balances},
550
553
  substrates_available=None not in substrates,
551
554
  )
@@ -583,6 +586,7 @@ def _fetch_lending_reads(
583
586
  balance_fuse_market_ids=[bf.market_id for bf in markets.balance_fuses],
584
587
  market_substrates=markets.market_substrates,
585
588
  aave_pools=markets.aave_pools,
589
+ comets=markets.comets,
586
590
  ),
587
591
  )
588
592
  morpho_unread: list[str] = []
@@ -668,7 +672,7 @@ def fetch_vault_data(
668
672
  _safe_call, partial(plasma_vault.read_stored_markets_total, ctx)
669
673
  )
670
674
  f_markets = pool.submit(
671
- _fetch_market_reads, ctx, plasma_vault, with_aave_pools=lending
675
+ _fetch_market_reads, ctx, plasma_vault, with_lending_venues=lending
672
676
  )
673
677
 
674
678
  markets = f_markets.result()
@@ -20,7 +20,12 @@ from ipor_fusion.readers.aave_v3 import (
20
20
  AaveV3PositionBreakdown,
21
21
  AaveV3Reader,
22
22
  )
23
- from ipor_fusion.readers.lending_health import AAVE_V3_MARKET_IDS, MORPHO_MARKET_IDS
23
+ from ipor_fusion.readers.compound_v3 import CompoundV3FuseReader
24
+ from ipor_fusion.readers.lending_health import (
25
+ AAVE_V3_MARKET_IDS,
26
+ COMPOUND_V3_MARKET_IDS,
27
+ MORPHO_MARKET_IDS,
28
+ )
24
29
  from ipor_fusion.readers.morpho import (
25
30
  MorphoPositionBreakdown,
26
31
  MorphoReader,
@@ -152,6 +157,28 @@ def _fetch_aave_pools(
152
157
  }
153
158
 
154
159
 
160
+ def _fetch_comets(
161
+ ctx: Web3Context, balance_fuses: list[BalanceFuse]
162
+ ) -> dict[MarketId, ChecksumAddress]:
163
+ """Map every Compound V3 market to the Comet its balance fuse reads. A balance
164
+ fuse that names no Comet reverts or answers with empty data and is left out."""
165
+ fuses = {
166
+ bf.market_id: bf.fuse
167
+ for bf in balance_fuses
168
+ if bf.market_id in COMPOUND_V3_MARKET_IDS
169
+ }
170
+ if not fuses:
171
+ return {}
172
+ comets = Multicall3(ctx).try_aggregate(
173
+ [CompoundV3FuseReader(fuse).comet() for fuse in fuses.values()]
174
+ )
175
+ return {
176
+ mid: comet
177
+ for mid, comet in zip(fuses, comets, strict=True)
178
+ if comet is not None
179
+ }
180
+
181
+
155
182
  def _fetch_aave_positions(
156
183
  ctx: Web3Context,
157
184
  pool: ThreadPoolExecutor,