ipor-fusion 4.0.0.dev43__tar.gz → 4.0.0.dev49__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 (287) hide show
  1. ipor_fusion-4.0.0.dev49/MIGRATION_4.md +151 -0
  2. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/PKG-INFO +5 -1
  3. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/README.md +4 -0
  4. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/pyproject.toml +2 -1
  5. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cache.py +32 -12
  6. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/explorer.py +4 -4
  7. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/vault_data.py +11 -10
  8. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/vault_info_golden/clean.json +2 -1
  9. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/vault_info_golden/clean.txt +1 -1
  10. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/vault_info_golden/rich.json +4 -2
  11. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/vault_info_golden/rich.txt +3 -3
  12. ipor_fusion-4.0.0.dev49/tests/test_cache.py +99 -0
  13. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_vault_info_golden.py +5 -1
  14. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_explorer.py +1 -1
  15. ipor_fusion-4.0.0.dev43/tests/test_cache.py +0 -44
  16. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.claude-plugin/marketplace.json +0 -0
  17. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.claude-plugin/plugin.json +0 -0
  18. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.env.example +0 -0
  19. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.gemini/settings.json +0 -0
  20. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.git-blame-ignore-revs +0 -0
  21. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/cd.yml +0 -0
  22. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/ci.yml +0 -0
  23. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/commit-lint.yml +0 -0
  24. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/notify-slack.yml +0 -0
  25. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/python-build.yml +0 -0
  26. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/python-publish.yml +0 -0
  27. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/release.yml +0 -0
  28. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.github/workflows/report-slack.yml +0 -0
  29. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.gitignore +0 -0
  30. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.mcp.json +0 -0
  31. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.pre-commit-config.yaml +0 -0
  32. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/.python-version +0 -0
  33. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/AGENTS.md +0 -0
  34. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/CHANGELOG.md +0 -0
  35. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/CLAUDE.md +0 -0
  36. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/CODEOWNERS +0 -0
  37. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/LICENSE +0 -0
  38. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/build.sh +0 -0
  39. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/CONTRIBUTING.md +0 -0
  40. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/README.md +0 -0
  41. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  42. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  43. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  44. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  45. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/external_state_margin_leg_base.py +0 -0
  46. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/examples/simple_aave_v3_supply_base.py +0 -0
  47. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/pytest.ini +0 -0
  48. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/scripts/commit-check.sh +0 -0
  49. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/scripts/crosschain_readiness.py +0 -0
  50. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/skills/ipor-deploy-vault/SKILL.md +0 -0
  51. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/__init__.py +0 -0
  52. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/about.py +0 -0
  53. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/chains.py +0 -0
  54. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/__init__.py +0 -0
  55. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  56. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/config_cmd.py +0 -0
  57. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/config_store.py +0 -0
  58. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/main.py +0 -0
  59. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/market_cmd.py +0 -0
  60. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/options.py +0 -0
  61. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  62. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  63. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/vault_health.py +0 -0
  64. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  65. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  66. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  67. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/config/__init__.py +0 -0
  68. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/config/roles.py +0 -0
  69. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/__init__.py +0 -0
  70. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/access.py +0 -0
  71. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/address.py +0 -0
  72. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/context.py +0 -0
  73. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/contract.py +0 -0
  74. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/erc20.py +0 -0
  75. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/external_state_executor.py +0 -0
  76. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/fee_manager.py +0 -0
  77. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/fuse_action.py +0 -0
  78. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/fusion_factory.py +0 -0
  79. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/logs.py +0 -0
  80. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/multicall.py +0 -0
  81. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/oracle.py +0 -0
  82. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/plasma_vault.py +0 -0
  83. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/rewards_manager.py +0 -0
  84. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/simulation.py +0 -0
  85. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  86. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/__init__.py +0 -0
  87. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  88. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  89. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  90. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  91. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  92. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  93. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  94. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  95. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  96. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/contracts.py +0 -0
  97. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/discovery.py +0 -0
  98. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/errors.py +0 -0
  99. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/lane.py +0 -0
  100. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/logs.py +0 -0
  101. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/messages.py +0 -0
  102. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/simulation.py +0 -0
  103. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  104. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  105. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  106. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  107. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  108. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/crosschain/transport.py +0 -0
  109. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/dependency_graph.py +0 -0
  110. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/entry_cli.py +0 -0
  111. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/entry_mcp.py +0 -0
  112. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/errors.py +0 -0
  113. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/field_docs.py +0 -0
  114. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/__init__.py +0 -0
  115. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  116. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/async_action.py +0 -0
  117. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/base.py +0 -0
  118. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  119. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  120. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  121. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  122. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  123. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/erc4626.py +0 -0
  124. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  125. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/events.py +0 -0
  126. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/external_state.py +0 -0
  127. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  128. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  129. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  130. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/hypercore.py +0 -0
  131. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  132. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/merkl.py +0 -0
  133. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/morpho.py +0 -0
  134. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  135. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  136. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/fuses/universal.py +0 -0
  137. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/guide/__init__.py +0 -0
  138. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/guide/architecture.md +0 -0
  139. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/guide/glossary.md +0 -0
  140. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/guide/invariants.md +0 -0
  141. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/guide/quickstart.md +0 -0
  142. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/market_ids.py +0 -0
  143. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/market_info.py +0 -0
  144. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/mcp/__init__.py +0 -0
  145. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/mcp/guide.py +0 -0
  146. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/mcp/models.py +0 -0
  147. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/mcp/server.py +0 -0
  148. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/morpho_api.py +0 -0
  149. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/py.typed +0 -0
  150. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/__init__.py +0 -0
  151. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/aave_v3.py +0 -0
  152. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/aave_v4.py +0 -0
  153. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/compound_v3.py +0 -0
  154. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/hypercore.py +0 -0
  155. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  156. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/lending_health.py +0 -0
  157. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/morpho.py +0 -0
  158. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  159. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/position_manager.py +0 -0
  160. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  161. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  162. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/readers/unpriceable.py +0 -0
  163. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/substrates.py +0 -0
  164. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/types.py +0 -0
  165. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/validation.py +0 -0
  166. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/src/ipor_fusion/vault_info.py +0 -0
  167. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/UniswapV3UniversalRouter.py +0 -0
  168. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_crosschain.py +0 -0
  169. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_crosschain_lifecycle.py +0 -0
  170. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_crosschain_recovery.py +0 -0
  171. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_euler_v2.py +0 -0
  172. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_foundry.py +0 -0
  173. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_guide.py +0 -0
  174. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_hypercore_model.py +0 -0
  175. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_hypercore_shadow.py +0 -0
  176. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_multicall.py +0 -0
  177. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_simulate.py +0 -0
  178. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/_validation.py +0 -0
  179. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/addresses.py +0 -0
  180. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/conftest.py +0 -0
  181. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/constants.py +0 -0
  182. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  183. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/fixtures/hypercore_market55.json +0 -0
  184. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_about.py +0 -0
  185. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_access_manager.py +0 -0
  186. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_chains.py +0 -0
  187. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_changelog.py +0 -0
  188. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_commands.py +0 -0
  189. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_config_store.py +0 -0
  190. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_helpers.py +0 -0
  191. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_hypercore.py +0 -0
  192. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_lending_health.py +0 -0
  193. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_market_morpho_blue.py +0 -0
  194. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_oracle_mapping.py +0 -0
  195. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_pending_requests_scan.py +0 -0
  196. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_role_accounts.py +0 -0
  197. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_vault_predates_getters.py +0 -0
  198. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_cli_vault_validate.py +0 -0
  199. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_contract_call.py +0 -0
  200. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_contract_encoder.py +0 -0
  201. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_ccip_events.py +0 -0
  202. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_ccip_indexer.py +0 -0
  203. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_ccip_quote.py +0 -0
  204. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_chainlink.py +0 -0
  205. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_contracts.py +0 -0
  206. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_encoding.py +0 -0
  207. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_errors.py +0 -0
  208. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_lane.py +0 -0
  209. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_layering.py +0 -0
  210. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_readiness.py +0 -0
  211. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_readiness_report.py +0 -0
  212. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_crosschain_transport.py +0 -0
  213. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_erc20.py +0 -0
  214. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_examples_composed_hypercore.py +0 -0
  215. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_examples_crosschain.py +0 -0
  216. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  217. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_examples_vaults.py +0 -0
  218. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_external_state_events.py +0 -0
  219. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_external_state_executor.py +0 -0
  220. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_fee_manager.py +0 -0
  221. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_fuse_encoding.py +0 -0
  222. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_guide.py +0 -0
  223. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_deployment.py +0 -0
  224. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_events.py +0 -0
  225. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_flow.py +0 -0
  226. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_live.py +0 -0
  227. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_outcome.py +0 -0
  228. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_readers.py +0 -0
  229. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_hypercore_simulation.py +0 -0
  230. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_logs.py +0 -0
  231. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_mcp_hypercore.py +0 -0
  232. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_mcp_models.py +0 -0
  233. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_mcp_server.py +0 -0
  234. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_mcp_vault_validate.py +0 -0
  235. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_multicall.py +0 -0
  236. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_oracle.py +0 -0
  237. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_oracle_mapping.py +0 -0
  238. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_plasma_vault.py +0 -0
  239. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_reader_encoding.py +0 -0
  240. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_readiness_selection.py +0 -0
  241. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_revert_decoding.py +0 -0
  242. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_rewards_manager.py +0 -0
  243. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_roles.py +0 -0
  244. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_sdk_layering.py +0 -0
  245. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  246. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  247. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  248. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_crosschain_canary.py +0 -0
  249. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_crosschain_hypercore.py +0 -0
  250. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  251. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  252. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  253. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  254. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  255. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  256. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  257. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_examples_arbitrum.py +0 -0
  258. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_examples_base.py +0 -0
  259. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_examples_crosschain.py +0 -0
  260. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_external_state_executor_base.py +0 -0
  261. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  262. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  263. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_morpho_blue.py +0 -0
  265. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_pilot_v3.py +0 -0
  266. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  267. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  268. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  269. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  270. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_readers_integration.py +0 -0
  271. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_release_funds.py +0 -0
  272. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_swap_on_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_solidity_compiler.py +0 -0
  275. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_solidity_mirrors.py +0 -0
  276. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_substrates.py +0 -0
  277. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_types_and_events.py +0 -0
  278. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_unpriceable_tokens.py +0 -0
  279. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_validation.py +0 -0
  280. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_vault_dep_graph.py +0 -0
  281. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_vault_info_onchain_base.py +0 -0
  282. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  283. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_vault_simulator_blocks.py +0 -0
  284. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_web3_context.py +0 -0
  285. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/test_withdraw_manager.py +0 -0
  286. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/tests/typed_client/consumer.py +0 -0
  287. {ipor_fusion-4.0.0.dev43 → ipor_fusion-4.0.0.dev49}/uv.lock +0 -0
@@ -0,0 +1,151 @@
1
+ # Migrating to ipor-fusion 4.0
2
+
3
+ 4.0 gives chain, market and role ids, the crosschain, HyperCore and Morpho ids,
4
+ vault/oracle/token addresses and transaction hashes a nominal type
5
+ (`typing.NewType`) across the public API, and checks the SDK, the examples and a
6
+ typed client with pyright's `reportArgumentType`. Other fields keep their types
7
+ (e.g. `VaultData.access_manager` is still `str`).
8
+
9
+ The wrappers do not change values: `ChainId(1)` is `1` at runtime. The
10
+ [behavior changes](#behavior-changes) below, however, apply to **every** caller,
11
+ type-checked or not. In particular, `fetch_market` and
12
+ `build_morpho_blue_market_json` no longer add a `0x` prefix themselves: pass
13
+ `parse_morpho_market_id(raw_id)`, never a bare 64-hex string. Type-checked code
14
+ (pyright, mypy, Pylance) also wraps literals and raw values as below. Values the SDK
15
+ returns already carry their type and go back in unwrapped.
16
+
17
+ All types are importable from `ipor_fusion` (and `ipor_fusion.types`);
18
+ `ChecksumAddress`, `HexAddress` and `HexStr` come from `eth_typing`,
19
+ `BlockIdentifier` from `web3.types`, and `USDC_TOKEN_INDEX` / `hip3_action_asset` from
20
+ `ipor_fusion.fuses.hypercore`. The wrappers label values; they do not validate byte
21
+ widths or address spelling.
22
+
23
+ ## Building the types
24
+
25
+ | Type | From a literal or raw value |
26
+ |---|---|
27
+ | `ChainId` | `ChainId(1)` |
28
+ | `MarketId` | an `IporFusionMarkets` member, or `MarketId(n)` |
29
+ | `RoleId` | a `Roles` member (accepted as is), or `RoleId(n)`; `Roles.resolve(name)` returns one |
30
+ | `Decimals`, `Period`, `Amount`, ... | `Decimals(8)`, `Period(0)`, `Amount(n)` (unchanged) |
31
+ | `ChecksumAddress` | `Web3.to_checksum_address(value)` |
32
+ | `HexAddress` | any `ChecksumAddress`, or `HexAddress(HexStr("0x..."))` for a lowercase address |
33
+ | `MorphoBlueMarketId` | `parse_morpho_market_id(value)`: 32 bytes or hex in any spelling, always `0x` + 64 lowercase digits |
34
+ | `TxHash` | `TxHash(HexStr("0x..."))` |
35
+ | crosschain ids | `CommandId(b)`, `MessageId(b)`, `AssetId(b)`, `OperationId(b)`, `TransferId(b)`, `ReturnRequestId(b)`, `PositionSetHash(b)` (bytes32); `ProposalId(n)`, `DeploymentRequestId(n)`, `StargateAssetId(n)` (int) |
36
+ | HyperCore ids | `HyperCoreTokenIndex(n)` (or `USDC_TOKEN_INDEX`), `HyperCoreAsset(n)` (or `hip3_action_asset(dex, index)`), `HyperCoreOrderId(n)`, `HyperCoreClientOrderId(n)`, `MarginTableId(n)`, `HyperCoreActionId(n)`, `PayloadHash(b)` |
37
+ | external state | `ProposalHash(b)` |
38
+ | `BlockIdentifier` | a block number, `"latest"` / `"pending"` / ..., or a block hash |
39
+
40
+ `MessageId` covers both a CCIP `messageId` and a LayerZero `guid`. `AssetId` is the
41
+ crosschain executor's `bytes32` asset id; a Stargate pool's `uint16` asset id is
42
+ `StargateAssetId`. `DeploymentRequestId` is a factory's `uint256` dispatcher request;
43
+ a `bytes32` return (recall) request is `ReturnRequestId`.
44
+
45
+ ## What changed, by area
46
+
47
+ **Vault reads and validation.** `fetch_vault_data(chain_id=)`, `fetch_deployment_info`,
48
+ `build_vault_info_json`, `ensure_supported_chain`, `fetch_vault_lending_health` take
49
+ `ChainId`; vault and oracle addresses
50
+ (`validate_vault`, `fetch_deployment_info`, `build_vault_info_json`,
51
+ `fetch_unpriceable_priced_tokens`) take `ChecksumAddress`; `resolve_token_symbol`,
52
+ `resolve_token_decimals`, `get_contract_name`, `get_deployment_tx` take `HexAddress`
53
+ (a checksum address fits). The `resolve_name` / `resolve_symbol` callbacks receive a
54
+ `HexAddress` (a callback typed for `str` still fits). Market-keyed data is keyed by
55
+ `MarketId`: `VaultData.market_substrates`, `dependency_graph`, `fuse_markets`,
56
+ `morpho_positions`, `aave_positions`; the `VaultState` protocol, `validate_vault_state`
57
+ (`market_balances`), `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens`
58
+ and the `dependency_graph` helpers (`compute_update_reach`, `compute_update_groups`,
59
+ `find_orphan_fuse_markets`, `find_markets_missing_erc20_dependency`).
60
+ A custom `VaultState` implementation also annotates `asset` as `ChecksumAddress` and
61
+ its `unpriceable_priced_tokens` items' `market_id` as `MarketId`
62
+ (`UnpriceableToken`). `VaultData.asset`, `VaultValidation.vault` / `asset` are
63
+ `ChecksumAddress`,
64
+ `VaultValidation.chain_id` is `ChainId`; `LendingMarketHealth.market_id` and
65
+ `MiddlewarePricedToken.market_id` are `MarketId`; `Erc20Holding.address` and
66
+ `SubstrateInfo.address` are `HexAddress` (still `""` when a substrate has no address).
67
+ `LegacyPriceOracleMiddleware` takes `base_currency_decimals: Decimals`.
68
+ `get_logs_adaptive(chain_id=)` and `LogSource.get_logs` take `ChainId`;
69
+ `quote_ccip_native_fee(block=)` and `OracleMappingReader.asset_source_events` take a
70
+ `BlockIdentifier`.
71
+
72
+ **Markets and substrates.** Every `IporFusionMarkets` member is a `MarketId`.
73
+ `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
74
+
75
+ **Access.** `AccessManager.has_role`, `grant_role`, `revoke_role`,
76
+ `get_accounts_with_role` take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
77
+
78
+ **Morpho.** `MorphoBlueMarketId` now always carries `0x`; build it with
79
+ `parse_morpho_market_id` instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market`,
80
+ `build_morpho_blue_market_json` take it; `MorphoApiMarket.market_id`,
81
+ `VaultV1MarketAllocation.market_id`, `VaultV2Cap.market_id`,
82
+ `MorphoPositionBreakdown.market_id` (as read from substrates) and the keys of
83
+ `VaultV1Info.public_allocator_flow_caps` are canonical. `fetch_market`, `fetch_vault`,
84
+ `morpho_blue_address`, `build_*_json` take `ChainId`; `fetch_vault` takes a
85
+ `ChecksumAddress`. Every address on the `morpho_api` records is `ChecksumAddress`;
86
+ optional ones (`curator`, `guardian`, `fee_recipient`, `performance_fee_recipient`,
87
+ `management_fee_recipient`, `inner_vault`, `VaultFlowCap.admin`, the `VaultV2Cap`
88
+ market params) are `ChecksumAddress | None`.
89
+
90
+ **Crosschain.** The executor, dispatcher and factory wrappers, the lanes and
91
+ `StargateCrosschainCommandFuse.enter(request_id=)` take the crosschain ids above, and
92
+ their getters return them (`active_command`, `active_proposal_id`, `propose_balance`
93
+ on CCIP, `active_return` → `OperationId`, `asset_id`, `tracked_position_set_hash`,
94
+ `pending_request_by_chain`, `CrosschainLane.proposal_id_from_logs` /
95
+ `proposal_id_from_receipt`). Records carry them too: `Command`, `ActiveCommand`,
96
+ `ExecutorInitParams`, `BalanceObservation`, `LaneObservation`, `Observation`,
97
+ `CcipObservation`, `OutboundMessage`, `CcipMessageSent`, `Any2EVMMessage`,
98
+ `VerifierResult`, `Packet.guid`, `TaxiMessage`, `StargateChain`; and the helpers
99
+ `fetch_verifier_result(message_id=)`, `lz_receive_calldata(guid=)` and
100
+ `lz_compose_calldata(guid=)` take a `MessageId`. Decoded event values
101
+ (`ccip_events` → `CcipEvent.values`) stay a generic mapping: convert a value
102
+ before passing it on.
103
+
104
+ **HyperCore (preview).** The fuses, `HyperCoreSubstrates`, `HyperCoreReader`,
105
+ `read_hypercore_transfer_snapshot` and the helpers `system_address`, `read_index_of`,
106
+ `perp_dex_of`, `is_native_perp_asset`, `is_hip3_asset` take the HyperCore ids;
107
+ `HyperCorePerpAssetInfo`, `HyperCorePerpMarket`, `HyperCoreSpotLeg`,
108
+ `HyperCorePendingState` and `HyperCoreTransferSnapshot` carry them.
109
+
110
+ **External state.** `ExternalStateExecutor.proposal_hash` returns and `confirm_balance`
111
+ takes a `ProposalHash`; `BalanceProposal.proposal_hash` and `NavMark.proposal_hash`
112
+ are `ProposalHash`; `ExternalStateEvent.transaction_hash` is a `TxHash`.
113
+
114
+ **Errors and explorer.** `TransactionError(tx_hash=)` takes a `TxHash`,
115
+ `LogScanError(address=)` a `ChecksumAddress`; `get_deployment_tx` returns a `TxHash`.
116
+
117
+ **MCP response models** (Python constructors only; the JSON schemas are unchanged
118
+ except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
119
+ `MarketId`. Calling an MCP tool function directly from Python takes `ChainId`.
120
+
121
+ **Moved, still importable.** `FuseAction` lives in `ipor_fusion.core.fuse_action` and
122
+ `ZERO_ADDRESS` in `ipor_fusion.types`; both are re-exported from
123
+ `ipor_fusion.fuses.base` and `FuseAction` from `ipor_fusion`. The CLI's internal
124
+ `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
125
+ `vault_markets_view` and `oracle_mapping_view` (private names, not API).
126
+
127
+ ## Behavior changes
128
+
129
+ - The MCP `vault_info` document reports the vault address checksummed (the CLI
130
+ already did).
131
+ - `fusion market meta-morpho --json` and the MCP `market_meta_morpho` tool report
132
+ `null` instead of `""` for an absent curator, guardian or fee recipient; the MCP
133
+ schema's `curator` is nullable.
134
+ - The Morpho API client checksums every address it returns (market, vault,
135
+ allocation, adapter and cap records, and the `fusion market` / MCP market documents
136
+ built from them), so string comparisons against the API's own spelling change.
137
+ - It raises `MorphoApiError` for a market id or address it parses (market and V1
138
+ allocation ids, V1 flow-cap ids, vault/asset/owner/token/oracle/IRM/allocator
139
+ addresses) when the value is invalid, or absent where the field is optional in the
140
+ answer. A key the parser requires can instead raise `KeyError`, e.g. a `loanAsset`
141
+ without `address`. When matching a market's own allocation and flow cap, an id that
142
+ does not parse matches nothing, and a V1 flow cap without an id is skipped, as
143
+ before. `--market` rejects a malformed id.
144
+ - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
145
+ documents already were).
146
+ - `TransactionError.tx_hash` is `0x`-prefixed.
147
+ - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
148
+ a value that is not an address.
149
+ - The on-disk caches (`~/.cache/ipor-fusion/`) key addresses lowercased; older entries
150
+ are rekeyed on load, so a contract name cached for one spelling now also resolves
151
+ for the other.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev43
3
+ Version: 4.0.0.dev49
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
@@ -115,6 +115,10 @@ alt="Release">
115
115
  </tr>
116
116
  </table>
117
117
 
118
+ > **Upgrading from 3.x?** 4.0 types the public API's ids and addresses and changes
119
+ > a few behaviors (Morpho market ids, nullable optional addresses); see
120
+ > [MIGRATION_4.md](MIGRATION_4.md).
121
+
118
122
  ## Quickstart
119
123
 
120
124
  ### Install
@@ -69,6 +69,10 @@ alt="Release">
69
69
  </tr>
70
70
  </table>
71
71
 
72
+ > **Upgrading from 3.x?** 4.0 types the public API's ids and addresses and changes
73
+ > a few behaviors (Morpho market ids, nullable optional addresses); see
74
+ > [MIGRATION_4.md](MIGRATION_4.md).
75
+
72
76
  ## Quickstart
73
77
 
74
78
  ### Install
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev43"
3
+ version = "4.0.0.dev49"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -165,3 +165,4 @@ packages = ["src/ipor_fusion"]
165
165
  # semantic-release before `build_command`, so each wheel carries its own entry.
166
166
  [tool.hatch.build.targets.wheel.force-include]
167
167
  "CHANGELOG.md" = "ipor_fusion/CHANGELOG.md"
168
+ "MIGRATION_4.md" = "ipor_fusion/MIGRATION_4.md"
@@ -9,6 +9,8 @@ import os
9
9
  import threading
10
10
  from pathlib import Path
11
11
 
12
+ from eth_typing import HexAddress
13
+
12
14
 
13
15
  def _xdg_cache_home() -> Path:
14
16
  return Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
@@ -21,11 +23,35 @@ DEPLOYMENT_CACHE_FILE = CACHE_DIR / "deployment_cache.json"
21
23
  _cache_lock = threading.Lock()
22
24
 
23
25
 
26
+ def cache_key(scope: object, address: HexAddress) -> str:
27
+ """``<scope>:<address>`` with the address lowercased: one entry per address
28
+ whatever spelling the caller holds. ``scope`` is a chain id or a field name
29
+ (``symbol``, ``decimals``)."""
30
+ return f"{scope}:{address.lower()}"
31
+
32
+
33
+ def _canonical(entries: dict) -> dict:
34
+ """Entries keyed by `cache_key` spelling. Caches written before 4.0 keyed
35
+ the address as the caller spelled it; an entry already canonical wins over
36
+ a differently spelled duplicate."""
37
+ out: dict = {}
38
+ for key, value in entries.items():
39
+ scope, sep, address = key.rpartition(":")
40
+ canonical = cache_key(scope, address) if sep else key
41
+ if canonical == key or canonical not in out:
42
+ out[canonical] = value
43
+ return out
44
+
45
+
46
+ def _read(path: Path) -> dict:
47
+ if not path.exists():
48
+ return {}
49
+ return _canonical(json.loads(path.read_text(encoding="utf-8")))
50
+
51
+
24
52
  def load_contract_cache() -> dict[str, str]:
25
53
  with _cache_lock:
26
- if not CACHE_FILE.exists():
27
- return {}
28
- return json.loads(CACHE_FILE.read_text(encoding="utf-8")) # type: ignore[no-any-return]
54
+ return _read(CACHE_FILE)
29
55
 
30
56
 
31
57
  def save_contract_cache(cache: dict[str, str]) -> None:
@@ -37,9 +63,7 @@ def save_contract_cache(cache: dict[str, str]) -> None:
37
63
  def update_contract_cache(key: str, value: str) -> None:
38
64
  with _cache_lock:
39
65
  CACHE_DIR.mkdir(parents=True, exist_ok=True)
40
- existing: dict[str, str] = {}
41
- if CACHE_FILE.exists():
42
- existing = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
66
+ existing: dict[str, str] = _read(CACHE_FILE)
43
67
  existing[key] = value
44
68
  CACHE_FILE.write_text(json.dumps(existing, indent=2), encoding="utf-8")
45
69
 
@@ -49,17 +73,13 @@ _deployment_cache_lock = threading.Lock()
49
73
 
50
74
  def load_deployment_cache() -> dict[str, dict[str, int]]:
51
75
  with _deployment_cache_lock:
52
- if not DEPLOYMENT_CACHE_FILE.exists():
53
- return {}
54
- return json.loads(DEPLOYMENT_CACHE_FILE.read_text(encoding="utf-8")) # type: ignore[no-any-return]
76
+ return _read(DEPLOYMENT_CACHE_FILE)
55
77
 
56
78
 
57
79
  def update_deployment_cache(key: str, block: int, timestamp: int) -> None:
58
80
  with _deployment_cache_lock:
59
81
  CACHE_DIR.mkdir(parents=True, exist_ok=True)
60
- existing: dict[str, dict[str, int]] = {}
61
- if DEPLOYMENT_CACHE_FILE.exists():
62
- existing = json.loads(DEPLOYMENT_CACHE_FILE.read_text(encoding="utf-8"))
82
+ existing: dict[str, dict[str, int]] = _read(DEPLOYMENT_CACHE_FILE)
63
83
  existing[key] = {"block": block, "timestamp": timestamp}
64
84
  DEPLOYMENT_CACHE_FILE.write_text(
65
85
  json.dumps(existing, indent=2), encoding="utf-8"
@@ -12,7 +12,7 @@ from urllib.request import urlopen
12
12
 
13
13
  from eth_typing import HexAddress, HexStr
14
14
 
15
- from ipor_fusion.cache import load_contract_cache, update_contract_cache
15
+ from ipor_fusion.cache import cache_key, load_contract_cache, update_contract_cache
16
16
  from ipor_fusion.types import ChainId, TxHash
17
17
 
18
18
  ETHERSCAN_V2_URL = "https://api.etherscan.io/v2/api"
@@ -44,13 +44,13 @@ def get_contract_name(
44
44
  chain_id: ChainId, address: HexAddress, api_key: str | None = None
45
45
  ) -> str:
46
46
  cache = load_contract_cache()
47
- cache_key = f"{chain_id}:{address}"
47
+ key = cache_key(chain_id, address)
48
48
 
49
- if cached := cache.get(cache_key):
49
+ if cached := cache.get(key):
50
50
  return cached
51
51
 
52
52
  if name := _fetch_contract_name(chain_id, address, api_key):
53
- update_contract_cache(cache_key, name)
53
+ update_contract_cache(key, name)
54
54
  return name
55
55
  return ""
56
56
 
@@ -25,6 +25,7 @@ from web3.exceptions import ContractLogicError, TimeExhausted, Web3RPCError
25
25
  from web3.types import ChecksumAddress, HexStr
26
26
 
27
27
  from ipor_fusion.cache import (
28
+ cache_key,
28
29
  load_contract_cache,
29
30
  load_deployment_cache,
30
31
  update_contract_cache,
@@ -325,14 +326,14 @@ def resolve_token_symbol(ctx: Web3Context, address: HexAddress) -> str:
325
326
  """ERC-20 symbol, cached; ``"no contract"`` when nothing is deployed at
326
327
  ``address`` and ``""`` when the symbol cannot be read."""
327
328
  cache = load_contract_cache()
328
- cache_key = f"symbol:{address}"
329
- if cached := cache.get(cache_key):
329
+ key = cache_key("symbol", address)
330
+ if cached := cache.get(key):
330
331
  return cached
331
332
 
332
333
  checksum = Web3.to_checksum_address(address)
333
334
  code = ctx.web3.eth.get_code(checksum)
334
335
  if not code or code == b"":
335
- update_contract_cache(cache_key, _NO_CONTRACT)
336
+ update_contract_cache(key, _NO_CONTRACT)
336
337
  return _NO_CONTRACT
337
338
 
338
339
  try:
@@ -340,15 +341,15 @@ def resolve_token_symbol(ctx: Web3Context, address: HexAddress) -> str:
340
341
  except Exception:
341
342
  symbol = ""
342
343
  if symbol:
343
- update_contract_cache(cache_key, symbol)
344
+ update_contract_cache(key, symbol)
344
345
  return symbol
345
346
 
346
347
 
347
348
  def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> int | None:
348
349
  """Resolve ERC-20 decimals, cached. Returns None if address is not a contract."""
349
350
  cache = load_contract_cache()
350
- cache_key = f"decimals:{address}"
351
- if cached := cache.get(cache_key):
351
+ key = cache_key("decimals", address)
352
+ if cached := cache.get(key):
352
353
  return int(cached)
353
354
 
354
355
  checksum = Web3.to_checksum_address(address)
@@ -360,7 +361,7 @@ def resolve_token_decimals(ctx: Web3Context, address: HexAddress) -> int | None:
360
361
  decimals = ERC20(ctx, checksum).decimals().call()
361
362
  except Exception:
362
363
  return None
363
- update_contract_cache(cache_key, str(decimals))
364
+ update_contract_cache(key, str(decimals))
364
365
  return decimals
365
366
 
366
367
 
@@ -1053,9 +1054,9 @@ def fetch_deployment_info(
1053
1054
  Base/Optimism on the free Etherscan tier). ``None`` means either success or
1054
1055
  a benign empty response.
1055
1056
  """
1056
- cache_key = f"{chain_id}:{vault_address}"
1057
+ key = cache_key(chain_id, vault_address)
1057
1058
  cache = load_deployment_cache()
1058
- if entry := cache.get(cache_key):
1059
+ if entry := cache.get(key):
1059
1060
  return entry["block"], entry["timestamp"], None
1060
1061
 
1061
1062
  tx_hash, error = get_deployment_tx(chain_id, vault_address, api_key)
@@ -1067,7 +1068,7 @@ def fetch_deployment_info(
1067
1068
  block_number: int = tx["blockNumber"]
1068
1069
  block_info = ctx.web3.eth.get_block(block_number)
1069
1070
  timestamp: int = block_info["timestamp"]
1070
- update_deployment_cache(cache_key, block_number, timestamp)
1071
+ update_deployment_cache(key, block_number, timestamp)
1071
1072
  return block_number, timestamp, None
1072
1073
  except Exception:
1073
1074
  return None, None, "rpc-fetch-failed"
@@ -69,7 +69,8 @@
69
69
  "AAVE_V3 (1)": [
70
70
  {
71
71
  "address": "0x00000000000000000000000000000000000000a1",
72
- "symbol": "USDC"
72
+ "symbol": "USDC",
73
+ "contract": "FiatTokenProxy"
73
74
  }
74
75
  ]
75
76
  },
@@ -36,7 +36,7 @@ Balance Fuses (1):
36
36
 
37
37
  Substrates per Market:
38
38
  AAVE_V3 (1):
39
- 0x00000000000000000000000000000000000000a1 (symbol=USDC)
39
+ 0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
40
40
 
41
41
  ERC20 Balances (vault holdings):
42
42
  (no ERC20_VAULT_BALANCE market)
@@ -143,7 +143,8 @@
143
143
  "AAVE_V3 (1)": [
144
144
  {
145
145
  "address": "0x00000000000000000000000000000000000000a1",
146
- "symbol": "USDC"
146
+ "symbol": "USDC",
147
+ "contract": "FiatTokenProxy"
147
148
  }
148
149
  ],
149
150
  "MORPHO (14)": [
@@ -155,7 +156,8 @@
155
156
  "ERC20_VAULT_BALANCE (7)": [
156
157
  {
157
158
  "address": "0x00000000000000000000000000000000000000a1",
158
- "symbol": "USDC"
159
+ "symbol": "USDC",
160
+ "contract": "FiatTokenProxy"
159
161
  },
160
162
  {
161
163
  "address": "0x00000000000000000000000000000000000000b1",
@@ -53,17 +53,17 @@ Dependency Balance Graph:
53
53
 
54
54
  Substrates per Market:
55
55
  AAVE_V3 (1):
56
- 0x00000000000000000000000000000000000000a1 (symbol=USDC)
56
+ 0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
57
57
  MORPHO (14):
58
58
  0xabababababababababababababababababababababababababababababababab (morpho_market_id)
59
59
  ERC20_VAULT_BALANCE (7):
60
- 0x00000000000000000000000000000000000000a1 (symbol=USDC)
60
+ 0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
61
61
  0x00000000000000000000000000000000000000b1 (symbol=STALE)
62
62
  0x00000000000000000000000000000000000000b2 (symbol=NOPRICE)
63
63
  0x00000000000000000000000000000000000000b3 (symbol=EMPTY)
64
64
  0x00000000000000000000000000000000000000b4 (symbol=BROKEN)
65
65
  AAVE_V3 (1):
66
- 0x00000000000000000000000000000000000000a1 (symbol=USDC)
66
+ 0x00000000000000000000000000000000000000a1 (symbol=USDC, contract=FiatTokenProxy)
67
67
 
68
68
  ERC20 Balances (vault holdings):
69
69
  Token Symbol Balance Price Note
@@ -0,0 +1,99 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from ipor_fusion import cache
6
+ from ipor_fusion.cache import load_contract_cache, save_contract_cache
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def _patch_paths(tmp_path, monkeypatch):
11
+ cache_dir = tmp_path / ".cache"
12
+ monkeypatch.setattr(cache, "CACHE_DIR", cache_dir)
13
+ monkeypatch.setattr(cache, "CACHE_FILE", cache_dir / "contract_cache.json")
14
+
15
+
16
+ class TestLoadContractCache:
17
+ def test_returns_empty_dict_when_no_file(self):
18
+ assert load_contract_cache() == {}
19
+
20
+ def test_loads_existing_cache(self, tmp_path):
21
+ cache_dir = tmp_path / ".cache"
22
+ cache_dir.mkdir()
23
+ cache = {"0xABC": "ERC20", "0xDEF": "PlasmaVault"}
24
+ (cache_dir / "contract_cache.json").write_text(
25
+ json.dumps(cache), encoding="utf-8"
26
+ )
27
+
28
+ assert load_contract_cache() == cache
29
+
30
+
31
+ class TestSaveContractCache:
32
+ def test_creates_directory_and_writes_json(self, tmp_path):
33
+ cache = {"0xABC": "ERC20"}
34
+ save_contract_cache(cache)
35
+
36
+ cache_file = tmp_path / ".cache" / "contract_cache.json"
37
+ assert cache_file.exists()
38
+ assert json.loads(cache_file.read_text(encoding="utf-8")) == cache
39
+
40
+ def test_roundtrip(self):
41
+ original = {"0x111": "TokenA", "0x222": "TokenB"}
42
+ save_contract_cache(original)
43
+ loaded = load_contract_cache()
44
+ assert loaded == original
45
+
46
+
47
+ class TestCanonicalKeys:
48
+ CHECKSUM = "0xAbCdEf0123456789aBcDeF0123456789AbCdEf01"
49
+
50
+ def _write(self, tmp_path, name, entries):
51
+ cache_dir = tmp_path / ".cache"
52
+ cache_dir.mkdir(exist_ok=True)
53
+ (cache_dir / name).write_text(json.dumps(entries), encoding="utf-8")
54
+
55
+ def test_a_key_lowercases_only_the_address(self):
56
+ assert cache.cache_key("symbol", self.CHECKSUM) == (
57
+ "symbol:" + self.CHECKSUM.lower()
58
+ )
59
+ assert cache.cache_key(1, self.CHECKSUM) == "1:" + self.CHECKSUM.lower()
60
+
61
+ def test_a_legacy_key_spelled_by_the_caller_still_hits(self, tmp_path):
62
+ self._write(tmp_path, "contract_cache.json", {f"1:{self.CHECKSUM}": "Vault"})
63
+ assert load_contract_cache()[cache.cache_key(1, self.CHECKSUM)] == "Vault"
64
+
65
+ def test_an_already_canonical_entry_wins_over_a_duplicate(self, tmp_path):
66
+ self._write(
67
+ tmp_path,
68
+ "contract_cache.json",
69
+ {
70
+ f"symbol:{self.CHECKSUM.lower()}": "NEW",
71
+ f"symbol:{self.CHECKSUM}": "OLD",
72
+ },
73
+ )
74
+ assert load_contract_cache() == {f"symbol:{self.CHECKSUM.lower()}": "NEW"}
75
+
76
+ def test_an_update_rewrites_legacy_keys(self, tmp_path):
77
+ self._write(tmp_path, "contract_cache.json", {f"1:{self.CHECKSUM}": "Vault"})
78
+ cache.update_contract_cache(cache.cache_key("decimals", self.CHECKSUM), "6")
79
+ on_disk = json.loads(
80
+ (tmp_path / ".cache" / "contract_cache.json").read_text(encoding="utf-8")
81
+ )
82
+ assert on_disk == {
83
+ f"1:{self.CHECKSUM.lower()}": "Vault",
84
+ f"decimals:{self.CHECKSUM.lower()}": "6",
85
+ }
86
+
87
+ def test_deployment_entries_are_canonical(self, tmp_path, monkeypatch):
88
+ monkeypatch.setattr(
89
+ cache,
90
+ "DEPLOYMENT_CACHE_FILE",
91
+ tmp_path / ".cache" / "deployment_cache.json",
92
+ )
93
+ self._write(
94
+ tmp_path,
95
+ "deployment_cache.json",
96
+ {f"1:{self.CHECKSUM}": {"block": 5, "timestamp": 6}},
97
+ )
98
+ loaded = cache.load_deployment_cache()
99
+ assert loaded[cache.cache_key(1, self.CHECKSUM)] == {"block": 5, "timestamp": 6}
@@ -362,6 +362,10 @@ def test_vault_info_output_matches_fixture(scenario: str, json_output: bool) ->
362
362
  json.loads(rendered)
363
363
 
364
364
 
365
+ # The CLI's cached lookup matches an address in any spelling; so does this one.
366
+ _NAMES_BY_ADDRESS = {address.lower(): name for address, name in NAMES.items()}
367
+
368
+
365
369
  @pytest.mark.usefixtures("isolated_cache")
366
370
  @pytest.mark.parametrize("scenario", sorted(SCENARIOS))
367
371
  def test_public_builder_renders_the_cli_json(scenario: str) -> None:
@@ -377,7 +381,7 @@ def test_public_builder_renders_the_cli_json(scenario: str) -> None:
377
381
  VAULT,
378
382
  1,
379
383
  "ethereum",
380
- resolve_name=lambda address: NAMES.get(address, ""),
384
+ resolve_name=lambda address: _NAMES_BY_ADDRESS.get(address.lower(), ""),
381
385
  )
382
386
  expected = (FIXTURES / f"{scenario}.json").read_text(encoding="utf-8")
383
387
  assert json.dumps(result, indent=2) + "\n" == expected
@@ -59,7 +59,7 @@ class TestGetContractName:
59
59
  cache_file = tmp_path / ".cache" / "contract_cache.json"
60
60
  assert cache_file.exists()
61
61
  cached = json.loads(cache_file.read_text(encoding="utf-8"))
62
- assert cached["1:0xDEF"] == "PlasmaVault"
62
+ assert cached["1:0xdef"] == "PlasmaVault"
63
63
 
64
64
  def test_returns_empty_for_unsupported_chain(self):
65
65
  result = get_contract_name(99999, "0xABC", api_key="key123")
@@ -1,44 +0,0 @@
1
- import json
2
-
3
- import pytest
4
-
5
- from ipor_fusion import cache
6
- from ipor_fusion.cache import load_contract_cache, save_contract_cache
7
-
8
-
9
- @pytest.fixture(autouse=True)
10
- def _patch_paths(tmp_path, monkeypatch):
11
- cache_dir = tmp_path / ".cache"
12
- monkeypatch.setattr(cache, "CACHE_DIR", cache_dir)
13
- monkeypatch.setattr(cache, "CACHE_FILE", cache_dir / "contract_cache.json")
14
-
15
-
16
- class TestLoadContractCache:
17
- def test_returns_empty_dict_when_no_file(self):
18
- assert load_contract_cache() == {}
19
-
20
- def test_loads_existing_cache(self, tmp_path):
21
- cache_dir = tmp_path / ".cache"
22
- cache_dir.mkdir()
23
- cache = {"0xABC": "ERC20", "0xDEF": "PlasmaVault"}
24
- (cache_dir / "contract_cache.json").write_text(
25
- json.dumps(cache), encoding="utf-8"
26
- )
27
-
28
- assert load_contract_cache() == cache
29
-
30
-
31
- class TestSaveContractCache:
32
- def test_creates_directory_and_writes_json(self, tmp_path):
33
- cache = {"0xABC": "ERC20"}
34
- save_contract_cache(cache)
35
-
36
- cache_file = tmp_path / ".cache" / "contract_cache.json"
37
- assert cache_file.exists()
38
- assert json.loads(cache_file.read_text(encoding="utf-8")) == cache
39
-
40
- def test_roundtrip(self):
41
- original = {"0x111": "TokenA", "0x222": "TokenB"}
42
- save_contract_cache(original)
43
- loaded = load_contract_cache()
44
- assert loaded == original