ipor-fusion 4.0.0.dev103__tar.gz → 4.0.0.dev108__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 (300) hide show
  1. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/AGENTS.md +29 -15
  2. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/MIGRATION_4.md +78 -3
  3. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/PKG-INFO +13 -5
  4. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/README.md +12 -4
  5. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/pyproject.toml +1 -1
  6. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/__init__.py +53 -37
  7. ipor_fusion-4.0.0.dev108/src/ipor_fusion/cache.py +204 -0
  8. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_info_view.py +4 -1
  9. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/context.py +15 -1
  10. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/__init__.py +89 -10
  11. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/__init__.py +4 -3
  12. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/lane.py +4 -4
  13. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/contracts.py +1 -1
  14. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/discovery.py +4 -4
  15. {ipor_fusion-4.0.0.dev103/src/ipor_fusion/fuses/crosschain → ipor_fusion-4.0.0.dev108/src/ipor_fusion/crosschain/fuses}/__init__.py +5 -4
  16. {ipor_fusion-4.0.0.dev103/src/ipor_fusion/fuses/crosschain → ipor_fusion-4.0.0.dev108/src/ipor_fusion/crosschain/fuses}/ccip.py +2 -2
  17. {ipor_fusion-4.0.0.dev103/src/ipor_fusion/fuses/crosschain → ipor_fusion-4.0.0.dev108/src/ipor_fusion/crosschain/fuses}/stargate.py +5 -5
  18. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/lane.py +3 -3
  19. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/__init__.py +5 -4
  20. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/lane.py +6 -6
  21. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/explorer.py +23 -11
  22. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/__init__.py +0 -34
  23. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_ids.py +1 -1
  24. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/server.py +4 -1
  25. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_data.py +60 -47
  26. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_crosschain.py +1 -1
  27. ipor_fusion-4.0.0.dev108/tests/test_cache.py +316 -0
  28. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_commands.py +12 -6
  29. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_helpers.py +15 -45
  30. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_market_morpho_blue.py +1 -8
  31. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_info_golden.py +7 -11
  32. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_encoding.py +5 -5
  33. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_lane.py +1 -1
  34. ipor_fusion-4.0.0.dev108/tests/test_crosschain_layering.py +212 -0
  35. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_explorer.py +38 -18
  36. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_live.py +1 -5
  37. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_public_api.py +65 -36
  38. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/typed_client/consumer.py +12 -0
  39. ipor_fusion-4.0.0.dev103/src/ipor_fusion/cache.py +0 -86
  40. ipor_fusion-4.0.0.dev103/tests/test_cache.py +0 -99
  41. ipor_fusion-4.0.0.dev103/tests/test_crosschain_layering.py +0 -117
  42. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.claude-plugin/marketplace.json +0 -0
  43. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.claude-plugin/plugin.json +0 -0
  44. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.env.example +0 -0
  45. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.gemini/settings.json +0 -0
  46. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.git-blame-ignore-revs +0 -0
  47. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/cd.yml +0 -0
  48. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/ci.yml +0 -0
  49. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/commit-lint.yml +0 -0
  50. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/notify-slack.yml +0 -0
  51. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-build.yml +0 -0
  52. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-publish.yml +0 -0
  53. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/release.yml +0 -0
  54. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.github/workflows/report-slack.yml +0 -0
  55. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.gitignore +0 -0
  56. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.mcp.json +0 -0
  57. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.pre-commit-config.yaml +0 -0
  58. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/.python-version +0 -0
  59. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/CHANGELOG.md +0 -0
  60. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/CLAUDE.md +0 -0
  61. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/CODEOWNERS +0 -0
  62. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/LICENSE +0 -0
  63. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/build.sh +0 -0
  64. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/CONTRIBUTING.md +0 -0
  65. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/README.md +0 -0
  66. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  67. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  68. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  69. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  70. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/external_state_margin_leg_base.py +0 -0
  71. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/examples/simple_aave_v3_supply_base.py +0 -0
  72. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/pytest.ini +0 -0
  73. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/scripts/commit-check.sh +0 -0
  74. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/scripts/crosschain_readiness.py +0 -0
  75. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/skills/ipor-deploy-vault/SKILL.md +0 -0
  76. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/about.py +0 -0
  77. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/chains.py +0 -0
  78. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/__init__.py +0 -0
  79. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  80. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_cmd.py +0 -0
  81. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_store.py +0 -0
  82. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/main.py +0 -0
  83. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/market_cmd.py +0 -0
  84. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/options.py +0 -0
  85. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  86. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  87. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_health.py +0 -0
  88. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  89. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  90. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/__init__.py +0 -0
  91. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/roles.py +0 -0
  92. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/__init__.py +0 -0
  93. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/access.py +0 -0
  94. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/address.py +0 -0
  95. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/contract.py +0 -0
  96. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/erc20.py +0 -0
  97. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/external_state_executor.py +0 -0
  98. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fee_manager.py +0 -0
  99. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse.py +0 -0
  100. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse_action.py +0 -0
  101. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fusion_factory.py +0 -0
  102. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/logs.py +0 -0
  103. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/multicall.py +0 -0
  104. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/oracle.py +0 -0
  105. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/plasma_vault.py +0 -0
  106. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/rewards_manager.py +0 -0
  107. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/simulation.py +0 -0
  108. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  109. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  110. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  111. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  112. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  113. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  114. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  115. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  116. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/errors.py +0 -0
  117. {ipor_fusion-4.0.0.dev103/src/ipor_fusion/fuses/crosschain → ipor_fusion-4.0.0.dev108/src/ipor_fusion/crosschain/fuses}/base.py +0 -0
  118. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/logs.py +0 -0
  119. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/messages.py +0 -0
  120. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/simulation.py +0 -0
  121. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  122. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  123. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  124. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/transport.py +0 -0
  125. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/dependency_graph.py +0 -0
  126. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_cli.py +0 -0
  127. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_mcp.py +0 -0
  128. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/errors.py +0 -0
  129. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/field_docs.py +0 -0
  130. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  131. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/async_action.py +0 -0
  132. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/base.py +0 -0
  133. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  134. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/erc4626.py +0 -0
  135. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  136. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/events.py +0 -0
  137. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state.py +0 -0
  138. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  139. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  140. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  141. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore.py +0 -0
  142. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  143. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/merkl.py +0 -0
  144. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/morpho.py +0 -0
  145. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  146. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  147. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/universal.py +0 -0
  148. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/__init__.py +0 -0
  149. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/architecture.md +0 -0
  150. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/glossary.md +0 -0
  151. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/invariants.md +0 -0
  152. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/quickstart.md +0 -0
  153. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_info.py +0 -0
  154. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/__init__.py +0 -0
  155. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/guide.py +0 -0
  156. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/models.py +0 -0
  157. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/morpho_api.py +0 -0
  158. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/py.typed +0 -0
  159. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/__init__.py +0 -0
  160. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v3.py +0 -0
  161. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v4.py +0 -0
  162. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/compound_v3.py +0 -0
  163. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore.py +0 -0
  164. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  165. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/lending_health.py +0 -0
  166. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/morpho.py +0 -0
  167. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  168. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  169. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  170. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/position_manager.py +0 -0
  171. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  172. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  173. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/unpriceable.py +0 -0
  174. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/substrates.py +0 -0
  175. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/types.py +0 -0
  176. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/validation.py +0 -0
  177. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_erc20.py +0 -0
  178. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_info.py +0 -0
  179. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_managers.py +0 -0
  180. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_positions.py +0 -0
  181. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_reads.py +0 -0
  182. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/UniswapV3UniversalRouter.py +0 -0
  183. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_lifecycle.py +0 -0
  184. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_recovery.py +0 -0
  185. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_euler_v2.py +0 -0
  186. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_foundry.py +0 -0
  187. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_guide.py +0 -0
  188. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_model.py +0 -0
  189. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_shadow.py +0 -0
  190. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_multicall.py +0 -0
  191. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_simulate.py +0 -0
  192. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/_validation.py +0 -0
  193. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/addresses.py +0 -0
  194. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/conftest.py +0 -0
  195. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/constants.py +0 -0
  196. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  197. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_market55.json +0 -0
  198. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.json +0 -0
  199. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  200. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.json +0 -0
  201. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  202. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_about.py +0 -0
  203. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_access_manager.py +0 -0
  204. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_chains.py +0 -0
  205. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_changelog.py +0 -0
  206. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_config_store.py +0 -0
  207. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_hypercore.py +0 -0
  208. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_lending_health.py +0 -0
  209. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_oracle_mapping.py +0 -0
  210. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_pending_requests_scan.py +0 -0
  211. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_role_accounts.py +0 -0
  212. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_predates_getters.py +0 -0
  213. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_validate.py +0 -0
  214. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_contract_call.py +0 -0
  215. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_contract_encoder.py +0 -0
  216. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_events.py +0 -0
  217. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_indexer.py +0 -0
  218. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_quote.py +0 -0
  219. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_chainlink.py +0 -0
  220. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_contracts.py +0 -0
  221. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_errors.py +0 -0
  222. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness.py +0 -0
  223. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness_report.py +0 -0
  224. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_transport.py +0 -0
  225. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_erc20.py +0 -0
  226. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_error_hierarchy.py +0 -0
  227. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_examples_composed_hypercore.py +0 -0
  228. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_examples_crosschain.py +0 -0
  229. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  230. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_examples_vaults.py +0 -0
  231. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_events.py +0 -0
  232. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_executor.py +0 -0
  233. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_fee_manager.py +0 -0
  234. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_fuse_encoding.py +0 -0
  235. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_fusion_instance.py +0 -0
  236. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_guide.py +0 -0
  237. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_deployment.py +0 -0
  238. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_events.py +0 -0
  239. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_flow.py +0 -0
  240. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_outcome.py +0 -0
  241. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_readers.py +0 -0
  242. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_simulation.py +0 -0
  243. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_logs.py +0 -0
  244. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_hypercore.py +0 -0
  245. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_models.py +0 -0
  246. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_server.py +0 -0
  247. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_vault_validate.py +0 -0
  248. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_multicall.py +0 -0
  249. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_oracle.py +0 -0
  250. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_oracle_mapping.py +0 -0
  251. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_plasma_vault.py +0 -0
  252. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_reader_encoding.py +0 -0
  253. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_readiness_selection.py +0 -0
  254. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_revert_decoding.py +0 -0
  255. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_rewards_manager.py +0 -0
  256. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_roles.py +0 -0
  257. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_sdk_layering.py +0 -0
  258. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  259. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  260. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  261. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_canary.py +0 -0
  262. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hypercore.py +0 -0
  263. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  264. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  265. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  268. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  270. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_arbitrum.py +0 -0
  271. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_crosschain.py +0 -0
  273. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_external_state_executor_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  275. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  276. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  277. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_morpho_blue.py +0 -0
  278. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v3.py +0 -0
  279. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  280. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  281. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  282. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  283. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_readers_integration.py +0 -0
  284. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_release_funds.py +0 -0
  285. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_swap_on_base.py +0 -0
  286. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  287. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_compiler.py +0 -0
  288. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_mirrors.py +0 -0
  289. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_substrates.py +0 -0
  290. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_types_and_events.py +0 -0
  291. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_unpriceable_tokens.py +0 -0
  292. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_validation.py +0 -0
  293. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_vault_dep_graph.py +0 -0
  294. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base.py +0 -0
  295. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  296. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_vault_simulator_blocks.py +0 -0
  297. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_web3_context.py +0 -0
  298. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/test_withdraw_manager.py +0 -0
  299. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/tests/typed_client/rejected.py +0 -0
  300. {ipor_fusion-4.0.0.dev103 → ipor_fusion-4.0.0.dev108}/uv.lock +0 -0
@@ -155,8 +155,8 @@ the lifecycle matrix follows that.
155
155
  cases and non-obvious logic.
156
156
  - The public API is `ipor_fusion.__all__` (README, "Public API and stability").
157
157
  Every public symbol is imported and listed there; a symbol of a public
158
- subpackage (`core`, `config`, `fuses`, `fuses.crosschain`, `readers`,
159
- `crosschain`, `crosschain.ccip`, `crosschain.stargate`) also goes in that
158
+ subpackage (`core`, `config`, `fuses`, `readers`, `crosschain`,
159
+ `crosschain.fuses`, `crosschain.ccip`, `crosschain.stargate`) also goes in that
160
160
  subpackage's `__all__`. `ipor_fusion.guide` keeps its own namespace. The
161
161
  public modules (`about`, `chains`, `dependency_graph`, `errors`, `explorer`,
162
162
  `field_docs`, `market_ids`, `market_info`, `morpho_api`, `substrates`, `types`,
@@ -204,7 +204,13 @@ the lifecycle matrix follows that.
204
204
  document rendered from the snapshot alone (contract names via an injected
205
205
  `resolve_name`),
206
206
  and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
207
- `cache.py` the on-disk contract and deployment caches (`~/.cache/ipor-fusion/`).
207
+ `cache.py` the `ContractCache` protocol (`get`/`put` of immutable facts keyed
208
+ `CacheKey(chain_id, address, field)`), `FileContractCache` (one JSON file, atomic
209
+ replace, corrupt/unwritable degrades to memory) and `NullCache`; every SDK use goes
210
+ through `cache_get`/`cache_put` (a failing cache is a miss, never an error) on
211
+ `ctx.cache` (`Web3Context(cache=...)`, default `default_contract_cache()` under
212
+ `$XDG_CACHE_HOME/ipor-fusion/`) or `get_contract_name(cache=...)`. Never cache an
213
+ absence (no code, no answer): it can change at a later block.
208
214
  `morpho_api.py` the Morpho public GraphQL API client (`fetch_market`, `fetch_vault`,
209
215
  `PUBLIC_ALLOCATOR_ADDRESSES`); `market_info.py` — `build_morpho_blue_market_json`,
210
216
  `build_meta_morpho_json`, the `fusion market` JSON documents.
@@ -224,7 +230,7 @@ the lifecycle matrix follows that.
224
230
  (`FuseAction`), `fuse` (the `Fuse` base, `StakeFuse`, the shared validators and
225
231
  substrate packers), `address` (`_validate_not_zero_address`). `core` imports neither
226
232
  `fuses` nor `crosschain` (`tests/test_sdk_layering.py`)
227
- - `fuses/` — per-protocol fuse encoders (aave_v3, async_action, compound_v3, crosschain/,
233
+ - `fuses/` — per-protocol fuse encoders (aave_v3, async_action, compound_v3,
228
234
  erc4626, euler_v2, external_state, fluid_instadapp, gearbox_v3, merkl, morpho, ramses_v2,
229
235
  uniswap_v3, universal, `events.py`); `base.py` re-exports the `core/fuse.py` base and validators,
230
236
  `FuseAction` and `ZERO_ADDRESS` (from `types`). A fuse is a value: equality and
@@ -251,15 +257,20 @@ the lifecycle matrix follows that.
251
257
  (`CcipOffRamp.execute` is the permissionless delivery) and `ccip/indexer` fetches a
252
258
  message's verifier result from Chainlink's public indexers and builds that
253
259
  `manual_execution` call.
254
- `fuses/crosschain/` mirrors it (`base`, `stargate`, `ccip`). Names mirror
255
- the Solidity contracts and libraries. Adding a transport: a new subpackage plus one
256
- `LANES` entry. Layering: the fuse encoders import the wire codecs and wrappers; lanes,
257
- discovery, transports and the simulator import the encoders. A package `__init__`
258
- runs before its modules, so `ipor_fusion.crosschain`, `.stargate` and `.ccip`
259
- export only the fuse-free half; the rest is exported from `ipor_fusion` and
260
- imported by module path. `tests/test_crosschain_layering.py` pins this (no
261
- function-level or `TYPE_CHECKING` imports in either package); never work around
262
- a cycle with a lazy import, move the module instead.
260
+ The market fuse encoders live in `crosschain/fuses/` (`base`, `stargate`, `ccip`),
261
+ mirroring `contracts/fuses/crosschain/`; they build on `core/fuse.py`, so the
262
+ dependency direction is `core` <- `crosschain` and `fuses/` holds no crosschain code
263
+ (neither package imports the other). Names mirror the Solidity contracts and
264
+ libraries. Adding a transport: a new subpackage, its encoders in `crosschain/fuses/`
265
+ and one `LANES` entry. Layering: the encoders import the wire codecs and wrappers;
266
+ lanes, discovery, transports and the simulator import the encoders.
267
+ `ipor_fusion.crosschain` exports the whole surface (encoders, lanes, discovery,
268
+ transports, simulator). A package `__init__` runs before its modules and the
269
+ encoders import `ccip.contracts` / `stargate.layerzero`, so `.ccip` and `.stargate`
270
+ export only their codecs and wrappers. `tests/test_crosschain_layering.py` pins this:
271
+ the SDK's module-level import graph, with the implicit package-`__init__` edges, is
272
+ acyclic, and the crosschain package has no function-level or `TYPE_CHECKING`
273
+ imports; never work around a cycle with a lazy import, move the module instead.
263
274
  - `readers/` — read side: lending_health, oracle_mapping, position_manager, aave_v3,
264
275
  compound_v3, morpho, ramses_v2, uniswap_v3, unpriceable (middleware-priced tokens of
265
276
  granted substrates the oracle cannot price)
@@ -383,8 +394,11 @@ byte, e.g. Ebisu ZAPPER/REGISTRY).
383
394
  role-accounts, oracle-mapping). `vault info` runs independent fetch pipelines
384
395
  (vault/asset, fees, WithdrawManager, markets, roles) concurrently on a
385
396
  `ThreadPoolExecutor`, each a short chain of `Multicall3` batches; keep new
386
- reads inside a batch rather than adding per-call round trips. Contract names
387
- and token symbols are cached in `~/.cache/ipor-fusion/contract_cache.json`.
397
+ reads inside a batch rather than adding per-call round trips. Contract names,
398
+ token symbols and decimals and deployment blocks are cached per chain in
399
+ `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`<chain>:<address>:<field>`
400
+ keys); tests redirect it with `monkeypatch.setenv("XDG_CACHE_HOME", ...)` or
401
+ inject a cache.
388
402
 
389
403
  Every CLI command has an MCP tool (`fusion changelog` maps to `server_info`;
390
404
  the rest share names). Read-only: `vault_info`, `vault_validate`,
@@ -220,6 +220,27 @@ live in `ipor_fusion.core.fuse`; `ipor_fusion.fuses.base`, `ipor_fusion.fuses` a
220
220
  hash now cover every attribute a fuse stores, not only its class and address (see
221
221
  [Behavior changes](#behavior-changes)).
222
222
 
223
+ **Crosschain fuse encoders moved.** The crosschain market encoders moved from
224
+ `ipor_fusion.fuses.crosschain` into the crosschain package, and
225
+ `ipor_fusion.fuses` no longer re-exports them. There is no shim: the old paths
226
+ raise `ImportError`. The top-level names (`from ipor_fusion import ...`) are
227
+ unchanged.
228
+
229
+ | 3.x import | 4.0 import |
230
+ |---|---|
231
+ | `ipor_fusion.fuses.crosschain` | `ipor_fusion.crosschain.fuses` (or `ipor_fusion.crosschain`) |
232
+ | `ipor_fusion.fuses.crosschain.base` | `ipor_fusion.crosschain.fuses.base` |
233
+ | `ipor_fusion.fuses.crosschain.ccip` | `ipor_fusion.crosschain.fuses.ccip` |
234
+ | `ipor_fusion.fuses.crosschain.stargate` | `ipor_fusion.crosschain.fuses.stargate` |
235
+ | `from ipor_fusion.fuses import CcipCrosschainSupplyFuse` (and the other crosschain encoders, `SendParams`, `CrosschainSubstrate*`, `crosschain_market_id`) | `from ipor_fusion.crosschain import ...` or `from ipor_fusion import ...` |
236
+
237
+ `ipor_fusion.crosschain` now exports the whole crosschain surface: besides the
238
+ codecs and wrappers, the encoders, `CrosschainLane`, `CcipLane`, `StargateLane`,
239
+ the transports and chains, `discover_deployment`, `open_lane(s)`, `LANES` and
240
+ `CrosschainSimulator`, which 3.x exported only from `ipor_fusion`.
241
+ `ipor_fusion.crosschain.ccip` and `.stargate` keep exporting only their codecs
242
+ and wrappers.
243
+
223
244
  **Moved, still importable.** `FuseAction` lives in `ipor_fusion.core.fuse_action` and
224
245
  `ZERO_ADDRESS` in `ipor_fusion.types`; both are re-exported from
225
246
  `ipor_fusion.fuses.base` and `FuseAction` from `ipor_fusion`. The CLI's internal
@@ -313,6 +334,61 @@ argument (`chain_id=`) for another chain. An external signer still needs the rea
313
334
  target address (`call.to`) and builds the chain-qualified transaction itself, or
314
335
  gets it from `call.build_transaction(ctx)`.
315
336
 
337
+ ## Contract cache
338
+
339
+ Contract names, token symbols and decimals and deployment blocks are cached in a
340
+ `ContractCache`, injected instead of always written to `~/.cache`:
341
+
342
+ - `Web3Context(..., cache=...)` and `Web3Context.from_url(..., cache=...)` take one
343
+ (keyword-only); `ctx.cache` returns it, and `ctx.at_block(...)` keeps it. The
344
+ default (`None`) is `default_contract_cache()`, a `FileContractCache` at
345
+ `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`~/.cache/...` when unset),
346
+ resolved when used rather than at import. `NullCache()` keeps nothing;
347
+ `FileContractCache(path)` moves the file; any object with `get(keys)` and
348
+ `put(entries)` over `CacheKey(chain_id, address, field)` → `str` fits the
349
+ protocol (for example a shared store in a service).
350
+ - `resolve_token_symbol`, `resolve_token_decimals`, the snapshot's token metadata
351
+ (`fetch_vault_data(metadata=True)`) and `fetch_deployment_info` use `ctx.cache`.
352
+ `get_contract_name(chain_id, address, api_key=None, *, cache=None)` takes the cache
353
+ explicitly (default: the file cache); pass `cache=ctx.cache` to share the
354
+ context's policy. `build_vault_info_json` without `resolve_name` still names
355
+ contracts from the default file cache; pass `resolve_name` to avoid it.
356
+ - `ipor_fusion.cache` is a public module now and exports `ContractCache`,
357
+ `FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
358
+ `default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
359
+ are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
360
+ `load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
361
+ `DEPLOYMENT_CACHE_FILE`. `cache_key(chain_id, address, field)` returns a `CacheKey`
362
+ and raises `ValueError` for an address that is not 20 bytes of hex, so
363
+ `get_contract_name` does too.
364
+
365
+ **File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
366
+ `symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
367
+ load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
368
+ 3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
369
+ discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
370
+ is no longer read: a vault's deployment block is resolved again (it needs an
371
+ Etherscan key) and the old file can be deleted.
372
+
373
+ **Behavior.**
374
+
375
+ - The same address on two chains has two entries: a token's symbol or decimals read
376
+ on one chain is never served for another.
377
+ - Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
378
+ address without code, but no longer stores it, so a token deployed later (or seen at
379
+ a later block) resolves.
380
+ - A write merges with the file as it is on disk and replaces it atomically (a
381
+ temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
382
+ processes writing at once never leave a torn file; one of two concurrent writers may
383
+ lose its newest entry, which is read again later.
384
+ - A cache never fails a read: a corrupt or unreadable file reads as empty (logged
385
+ warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
386
+ directory (read-only file system) keeps the entries in memory for the process; a
387
+ custom cache that raises is treated as a miss.
388
+ - `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
389
+ transaction or its block cannot be read; a cache failure is no longer reported as
390
+ one (the deployment block is returned).
391
+
316
392
  ## Behavior changes
317
393
 
318
394
  - Two fuses are equal (and hash alike) only when they have the same class and the
@@ -350,9 +426,8 @@ gets it from `call.build_transaction(ctx)`.
350
426
  - `TransactionError.tx_hash` is `0x`-prefixed.
351
427
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
352
428
  a value that is not an address.
353
- - The on-disk caches (`~/.cache/ipor-fusion/`) key addresses lowercased; older entries
354
- are rekeyed on load, so a contract name cached for one spelling now also resolves
355
- for the other.
429
+ - The contract cache keys every entry by chain and lowercase address and no longer
430
+ caches an absence (see [Contract cache](#contract-cache)).
356
431
 
357
432
  ## Errors
358
433
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev103
3
+ Version: 4.0.0.dev108
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
@@ -377,15 +377,15 @@ renaming a name in it, or changing a signature incompatibly, waits for a major
377
377
  release; adding one does not.
378
378
 
379
379
  - **Public subpackages** declare their own `__all__`, each a subset of the top
380
- level: `ipor_fusion.core`, `config`, `fuses`, `fuses.crosschain`, `readers`,
381
- `crosschain`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
380
+ level: `ipor_fusion.core`, `config`, `fuses`, `readers`, `crosschain`,
381
+ `crosschain.fuses`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
382
382
  (the packaged docs) keeps its names in its own namespace.
383
383
  - **Public modules** stay importable for the top-level names they define:
384
- `about`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
384
+ `about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
385
385
  `market_ids`, `market_info`, `morpho_api`, `substrates`, `types`, `validation`,
386
386
  `vault_data` and `vault_info` (all under `ipor_fusion.`).
387
387
  - **Internal:** `ipor_fusion.cli`, `ipor_fusion.mcp`, every other module (e.g.
388
- `cache`, `vault_reads`, the submodules of the public subpackages), underscore
388
+ `vault_reads`, the submodules of the public subpackages), underscore
389
389
  names, and names in no `__all__`. They change without notice; the CLI and the
390
390
  MCP server are stable as commands and tools, not as Python modules.
391
391
  - **Preview features** (`PREVIEW_FEATURES` in `ipor_fusion.about`: crosschain,
@@ -495,6 +495,14 @@ fields as unavailable (`null`) rather than partial. A `LogSource` on the context
495
495
  a service that caches an explorer's log API, answers those scans instead
496
496
  (`Web3Context(log_source=…)`); when it raises `LogSourceUnavailable` the RPC scan runs.
497
497
 
498
+ Immutable contract facts (token symbols and decimals, contract names, deployment blocks)
499
+ are cached per chain and address in the context's `ContractCache`. The default,
500
+ `FileContractCache`, is `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json`
501
+ (`~/.cache/...`), written atomically and ignored when corrupt or unwritable. A service
502
+ passes `Web3Context(..., cache=NullCache())` to keep nothing, `FileContractCache(path)`
503
+ to move it, or its own `get` / `put` implementation; `get_contract_name(..., cache=)`
504
+ takes the same object.
505
+
498
506
  ## Development
499
507
 
500
508
  ```bash
@@ -329,15 +329,15 @@ renaming a name in it, or changing a signature incompatibly, waits for a major
329
329
  release; adding one does not.
330
330
 
331
331
  - **Public subpackages** declare their own `__all__`, each a subset of the top
332
- level: `ipor_fusion.core`, `config`, `fuses`, `fuses.crosschain`, `readers`,
333
- `crosschain`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
332
+ level: `ipor_fusion.core`, `config`, `fuses`, `readers`, `crosschain`,
333
+ `crosschain.fuses`, `crosschain.ccip` and `crosschain.stargate`. `ipor_fusion.guide`
334
334
  (the packaged docs) keeps its names in its own namespace.
335
335
  - **Public modules** stay importable for the top-level names they define:
336
- `about`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
336
+ `about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`, `field_docs`,
337
337
  `market_ids`, `market_info`, `morpho_api`, `substrates`, `types`, `validation`,
338
338
  `vault_data` and `vault_info` (all under `ipor_fusion.`).
339
339
  - **Internal:** `ipor_fusion.cli`, `ipor_fusion.mcp`, every other module (e.g.
340
- `cache`, `vault_reads`, the submodules of the public subpackages), underscore
340
+ `vault_reads`, the submodules of the public subpackages), underscore
341
341
  names, and names in no `__all__`. They change without notice; the CLI and the
342
342
  MCP server are stable as commands and tools, not as Python modules.
343
343
  - **Preview features** (`PREVIEW_FEATURES` in `ipor_fusion.about`: crosschain,
@@ -447,6 +447,14 @@ fields as unavailable (`null`) rather than partial. A `LogSource` on the context
447
447
  a service that caches an explorer's log API, answers those scans instead
448
448
  (`Web3Context(log_source=…)`); when it raises `LogSourceUnavailable` the RPC scan runs.
449
449
 
450
+ Immutable contract facts (token symbols and decimals, contract names, deployment blocks)
451
+ are cached per chain and address in the context's `ContractCache`. The default,
452
+ `FileContractCache`, is `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json`
453
+ (`~/.cache/...`), written atomically and ignored when corrupt or unwritable. A service
454
+ passes `Web3Context(..., cache=NullCache())` to keep nothing, `FileContractCache(path)`
455
+ to move it, or its own `get` / `put` implementation; `get_contract_name(..., cache=)`
456
+ takes the same object.
457
+
450
458
  ## Development
451
459
 
452
460
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev103"
3
+ version = "4.0.0.dev108"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -7,6 +7,15 @@ from ipor_fusion.about import (
7
7
  read_changelog,
8
8
  repository_url,
9
9
  )
10
+ from ipor_fusion.cache import (
11
+ CacheField,
12
+ CacheKey,
13
+ ContractCache,
14
+ FileContractCache,
15
+ NullCache,
16
+ cache_key,
17
+ default_contract_cache,
18
+ )
10
19
  from ipor_fusion.chains import (
11
20
  CHAIN_NAME_TO_ID,
12
21
  CHAIN_NAMES,
@@ -79,17 +88,24 @@ from ipor_fusion.crosschain import (
79
88
  EMPTY_COMMAND,
80
89
  INDEXER_URLS,
81
90
  INSUFFICIENT_NATIVE_FEE_SELECTOR,
91
+ LANES,
82
92
  PACKET_SENT_TOPIC,
93
+ SYNTHETIC_TOKEN_SOURCE,
83
94
  ActiveCommand,
84
95
  Any2EVMMessage,
85
96
  BalanceObservation,
86
97
  BusinessAction,
98
+ CcipChain,
99
+ CcipCommandType,
87
100
  CcipCommitteeVerifier,
101
+ CcipCrosschainCommandFuse,
88
102
  CcipCrosschainDispatcher,
89
103
  CcipCrosschainExecutor,
90
104
  CcipCrosschainFactory,
105
+ CcipCrosschainSupplyFuse,
91
106
  CcipEvent,
92
107
  CcipEventSpec,
108
+ CcipLane,
93
109
  CcipMessageSent,
94
110
  CcipMsgType,
95
111
  CcipObservation,
@@ -97,32 +113,57 @@ from ipor_fusion.crosschain import (
97
113
  CcipOnRamp,
98
114
  CcipRouteConfig,
99
115
  CcipRouter,
116
+ CcipSendParams,
100
117
  CcipTokenAdminRegistry,
101
118
  CcipTokenLane,
102
119
  CcipTokenPool,
120
+ CcipTransport,
103
121
  CcipVerifierResolver,
104
122
  CcvRequirements,
105
123
  Command,
106
124
  CommandStatus,
125
+ CrosschainClaimFuse,
126
+ CrosschainCommandFuse,
127
+ CrosschainDeployment,
107
128
  CrosschainDispatcher,
108
129
  CrosschainExecutor,
130
+ CrosschainExecutorInfo,
109
131
  CrosschainFactory,
132
+ CrosschainLane,
133
+ CrosschainSimulator,
134
+ CrosschainSubstrate,
135
+ CrosschainSubstrateLib,
136
+ CrosschainSubstrateType,
137
+ CrosschainSupplyFuse,
138
+ CrosschainTransport,
110
139
  CrosschainTransportKind,
140
+ DeliveryCall,
111
141
  DeploymentStatus,
112
142
  EnforcedOptionParam,
113
143
  EVMTokenAmount,
114
144
  ExecutorInitParams,
145
+ LaneFuses,
146
+ LaneObservation,
115
147
  MessageExecutionState,
116
148
  MessageV1,
117
149
  MsgType,
118
150
  Observation,
119
151
  OptionsBuilder,
152
+ OutboundMessage,
120
153
  Packet,
121
154
  SafetyConfig,
155
+ SendParams,
156
+ StargateChain,
157
+ StargateCrosschainCommandFuse,
158
+ StargateCrosschainCommandType,
122
159
  StargateCrosschainDispatcher,
123
160
  StargateCrosschainExecutor,
124
161
  StargateCrosschainFactory,
162
+ StargateCrosschainSupplyFuse,
163
+ StargateLane,
164
+ StargateSendParams,
125
165
  StargateTokenMessaging,
166
+ StargateTransport,
126
167
  TaxiMessage,
127
168
  TokenTransferV1,
128
169
  VerifierResult,
@@ -130,10 +171,14 @@ from ipor_fusion.crosschain import (
130
171
  ccip_events,
131
172
  ccip_receive_calldata,
132
173
  ccip_token_lane,
174
+ crosschain_market_id,
133
175
  decode_ccip_envelope,
134
176
  decode_ccip_event,
135
177
  decode_command,
136
178
  decode_envelope,
179
+ detect_transport,
180
+ discover_deployment,
181
+ discover_lane_fuses,
137
182
  encode_enforced_options,
138
183
  encode_oft_compose_msg,
139
184
  fetch_verifier_result,
@@ -141,29 +186,9 @@ from ipor_fusion.crosschain import (
141
186
  lz_compose_calldata,
142
187
  lz_receive_calldata,
143
188
  manual_execution,
144
- quote_ccip_native_fee,
145
- )
146
- from ipor_fusion.crosschain.ccip.lane import CcipLane
147
- from ipor_fusion.crosschain.ccip.transport import CcipChain, CcipTransport
148
- from ipor_fusion.crosschain.discovery import (
149
- LANES,
150
- CrosschainDeployment,
151
- CrosschainExecutorInfo,
152
- detect_transport,
153
- discover_deployment,
154
- discover_lane_fuses,
155
189
  open_lane,
156
190
  open_lanes,
157
- )
158
- from ipor_fusion.crosschain.lane import CrosschainLane, LaneFuses, LaneObservation
159
- from ipor_fusion.crosschain.simulation import CrosschainSimulator
160
- from ipor_fusion.crosschain.stargate.lane import StargateLane
161
- from ipor_fusion.crosschain.stargate.transport import StargateChain, StargateTransport
162
- from ipor_fusion.crosschain.transport import (
163
- SYNTHETIC_TOKEN_SOURCE,
164
- CrosschainTransport,
165
- DeliveryCall,
166
- OutboundMessage,
191
+ quote_ccip_native_fee,
167
192
  )
168
193
  from ipor_fusion.dependency_graph import (
169
194
  compute_update_groups,
@@ -206,17 +231,7 @@ from ipor_fusion.fuses import (
206
231
  AaveV3SupplyFuse,
207
232
  AsyncActionFuse,
208
233
  AsyncActionSubstrates,
209
- CcipCommandType,
210
- CcipCrosschainCommandFuse,
211
- CcipCrosschainSupplyFuse,
212
- CcipSendParams,
213
234
  CompoundV3SupplyFuse,
214
- CrosschainClaimFuse,
215
- CrosschainCommandFuse,
216
- CrosschainSubstrate,
217
- CrosschainSubstrateLib,
218
- CrosschainSubstrateType,
219
- CrosschainSupplyFuse,
220
235
  ERC4626SupplyFuse,
221
236
  EulerSwapDynamicParams,
222
237
  EulerSwapInitialState,
@@ -261,13 +276,8 @@ from ipor_fusion.fuses import (
261
276
  RamsesV2CollectFuse,
262
277
  RamsesV2ModifyPositionFuse,
263
278
  RamsesV2NewPositionFuse,
264
- SendParams,
265
279
  SettlementMode,
266
280
  StakeFuse,
267
- StargateCrosschainCommandFuse,
268
- StargateCrosschainCommandType,
269
- StargateCrosschainSupplyFuse,
270
- StargateSendParams,
271
281
  TimeInForce,
272
282
  UniswapV3ClosePositionEvent,
273
283
  UniswapV3CollectFuse,
@@ -279,7 +289,6 @@ from ipor_fusion.fuses import (
279
289
  UniversalTokenSwapperAbi,
280
290
  UniversalTokenSwapperFuse,
281
291
  UniversalTokenSwapperSubstrates,
282
- crosschain_market_id,
283
292
  decode_hypercore_event,
284
293
  euler_substrate,
285
294
  find_hypercore_events,
@@ -741,6 +750,13 @@ __all__ = [
741
750
  "decode_custom_error",
742
751
  "get_revert_reason",
743
752
  "register_custom_errors",
753
+ "CacheField",
754
+ "CacheKey",
755
+ "ContractCache",
756
+ "FileContractCache",
757
+ "NullCache",
758
+ "cache_key",
759
+ "default_contract_cache",
744
760
  "get_contract_name",
745
761
  "get_deployment_tx",
746
762
  "CHAIN_NAMES",
@@ -0,0 +1,204 @@
1
+ """Caches of immutable per-contract chain facts: contract names, token symbols
2
+ and decimals, deployment blocks. A `ContractCache` is injected through
3
+ `Web3Context(cache=...)` (and `get_contract_name(cache=...)`); the default is
4
+ `FileContractCache` at ``$XDG_CACHE_HOME/ipor-fusion/contract_cache.json``,
5
+ and `NullCache` disables caching.
6
+
7
+ A cache is an optimization, never a source of failure: the SDK reads and
8
+ writes it through `cache_get` / `cache_put`, which log and swallow any error a
9
+ cache raises, and `FileContractCache` itself degrades to an in-memory map when
10
+ its file is corrupt, unreadable or unwritable."""
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ import os
17
+ import tempfile
18
+ import threading
19
+ from collections.abc import Iterable, Mapping
20
+ from pathlib import Path
21
+ from typing import Literal, NamedTuple, Protocol, cast, get_args
22
+
23
+ from eth_typing import HexAddress
24
+
25
+ from ipor_fusion.types import ChainId, parse_hex_address
26
+
27
+ _logger = logging.getLogger(__name__)
28
+
29
+ CacheField = Literal[
30
+ "name", "symbol", "decimals", "deployment_block", "deployment_timestamp"
31
+ ]
32
+ _FIELDS: frozenset[str] = frozenset(get_args(CacheField))
33
+
34
+
35
+ class CacheKey(NamedTuple):
36
+ """One fact about the contract at ``address`` on ``chain_id``. Build it
37
+ with `cache_key`, which normalizes the address to lowercase."""
38
+
39
+ chain_id: ChainId
40
+ address: HexAddress
41
+ field: CacheField
42
+
43
+
44
+ def cache_key(chain_id: ChainId, address: HexAddress, field: CacheField) -> CacheKey:
45
+ return CacheKey(chain_id, parse_hex_address(address), field)
46
+
47
+
48
+ class ContractCache(Protocol):
49
+ """Immutable facts keyed by chain, address and field, as strings. Store
50
+ only what cannot change for that key; absence (no code, no answer) is
51
+ never stored."""
52
+
53
+ def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
54
+ """The cached values of ``keys``; a miss is absent from the result."""
55
+ ...
56
+
57
+ def put(self, entries: Mapping[CacheKey, str]) -> None: ...
58
+
59
+
60
+ class NullCache:
61
+ """A `ContractCache` that stores nothing: every read goes to the chain or
62
+ the explorer."""
63
+
64
+ def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
65
+ return {}
66
+
67
+ def put(self, entries: Mapping[CacheKey, str]) -> None:
68
+ return None
69
+
70
+
71
+ def _encode(key: CacheKey) -> str:
72
+ return f"{key.chain_id}:{key.address}:{key.field}"
73
+
74
+
75
+ def _decode(raw: str) -> CacheKey | None:
76
+ """The key of one stored entry, ``None`` for an entry that cannot be
77
+ attributed to a chain, an address and a field. A ``<chain>:<address>`` entry is a contract
78
+ name (the 3.x layout); 3.x token entries (``symbol:<address>``) carry no
79
+ chain and are dropped."""
80
+ parts = raw.split(":")
81
+ if len(parts) == 2:
82
+ parts.append("name")
83
+ if len(parts) != 3 or not parts[0].isdigit() or parts[2] not in _FIELDS:
84
+ return None
85
+ chain, address, field = parts
86
+ try:
87
+ return cache_key(
88
+ ChainId(int(chain)), parse_hex_address(address), cast(CacheField, field)
89
+ )
90
+ except ValueError:
91
+ return None
92
+
93
+
94
+ class FileContractCache:
95
+ """A `ContractCache` in one JSON file, loaded once per instance.
96
+
97
+ A write merges into the file as it is on disk (another process may have
98
+ added entries) and replaces it atomically (a temporary file in the same
99
+ directory, then `os.replace`), so a crash or a concurrent writer never
100
+ leaves a torn file; concurrent writers may drop each other's latest entry,
101
+ which is refetched later. A corrupt or unreadable file reads as empty and
102
+ is rewritten by the next write; a failed write is logged and the entry
103
+ kept in memory only."""
104
+
105
+ def __init__(self, path: Path) -> None:
106
+ self.path = path
107
+ self._lock = threading.Lock()
108
+ self._entries: dict[CacheKey, str] | None = None
109
+
110
+ def get(self, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
111
+ with self._lock:
112
+ entries = self._loaded()
113
+ return {key: entries[key] for key in keys if key in entries}
114
+
115
+ def put(self, entries: Mapping[CacheKey, str]) -> None:
116
+ if not entries:
117
+ return
118
+ with self._lock:
119
+ merged = self._read() | (self._entries or {}) | dict(entries)
120
+ self._entries = merged
121
+ try:
122
+ self._write(merged)
123
+ except OSError as exc:
124
+ _logger.warning("contract cache %s not written: %s", self.path, exc)
125
+
126
+ def _loaded(self) -> dict[CacheKey, str]:
127
+ if self._entries is None:
128
+ self._entries = self._read()
129
+ return self._entries
130
+
131
+ def _read(self) -> dict[CacheKey, str]:
132
+ try:
133
+ raw = json.loads(self.path.read_text(encoding="utf-8"))
134
+ except FileNotFoundError:
135
+ return {}
136
+ except (OSError, ValueError) as exc:
137
+ _logger.warning("contract cache %s unreadable, ignored: %s", self.path, exc)
138
+ return {}
139
+ if not isinstance(raw, dict):
140
+ _logger.warning(
141
+ "contract cache %s is not a JSON object, ignored", self.path
142
+ )
143
+ return {}
144
+ entries: dict[CacheKey, str] = {}
145
+ for raw_key, value in raw.items():
146
+ key = _decode(raw_key)
147
+ if key is not None and isinstance(value, str):
148
+ # A canonical entry wins over a 3.x one spelled otherwise.
149
+ if key not in entries or _encode(key) == raw_key:
150
+ entries[key] = value
151
+ return entries
152
+
153
+ def _write(self, entries: dict[CacheKey, str]) -> None:
154
+ self.path.parent.mkdir(parents=True, exist_ok=True)
155
+ payload = json.dumps(
156
+ {_encode(key): entries[key] for key in sorted(entries)}, indent=2
157
+ )
158
+ fd, tmp = tempfile.mkstemp(
159
+ dir=self.path.parent, prefix=f".{self.path.name}.", suffix=".tmp"
160
+ )
161
+ try:
162
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
163
+ fh.write(payload)
164
+ os.replace(tmp, self.path)
165
+ except BaseException:
166
+ Path(tmp).unlink(missing_ok=True)
167
+ raise
168
+
169
+
170
+ def default_cache_path() -> Path:
171
+ """``$XDG_CACHE_HOME/ipor-fusion/contract_cache.json``
172
+ (``~/.cache/...`` when unset), resolved at call time."""
173
+ home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
174
+ return home / "ipor-fusion" / "contract_cache.json"
175
+
176
+
177
+ _defaults: dict[Path, FileContractCache] = {}
178
+ _defaults_lock = threading.Lock()
179
+
180
+
181
+ def default_contract_cache() -> FileContractCache:
182
+ """The process-wide `FileContractCache` at `default_cache_path`."""
183
+ path = default_cache_path()
184
+ with _defaults_lock:
185
+ if (cache := _defaults.get(path)) is None:
186
+ cache = _defaults[path] = FileContractCache(path)
187
+ return cache
188
+
189
+
190
+ def cache_get(cache: ContractCache, keys: Iterable[CacheKey]) -> dict[CacheKey, str]:
191
+ """``cache.get(keys)``; a cache that raises reads as all misses."""
192
+ try:
193
+ return cache.get(keys)
194
+ except Exception as exc:
195
+ _logger.warning("contract cache read failed, ignored: %s", exc)
196
+ return {}
197
+
198
+
199
+ def cache_put(cache: ContractCache, entries: Mapping[CacheKey, str]) -> None:
200
+ """``cache.put(entries)``; a cache that raises loses the entries."""
201
+ try:
202
+ cache.put(entries)
203
+ except Exception as exc:
204
+ _logger.warning("contract cache write failed, ignored: %s", exc)