ipor-fusion 4.0.0.dev106__tar.gz → 4.0.0.dev110__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 (299) hide show
  1. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/AGENTS.md +13 -4
  2. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/MIGRATION_4.md +62 -3
  3. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/PKG-INFO +11 -3
  4. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/README.md +10 -2
  5. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/pyproject.toml +1 -1
  6. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/__init__.py +16 -0
  7. ipor_fusion-4.0.0.dev110/src/ipor_fusion/cache.py +204 -0
  8. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_info_view.py +5 -4
  9. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_markets_view.py +19 -26
  10. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/context.py +15 -1
  11. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/explorer.py +23 -11
  12. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/server.py +4 -1
  13. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_data.py +67 -47
  14. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_info.py +8 -6
  15. ipor_fusion-4.0.0.dev110/tests/test_cache.py +362 -0
  16. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_commands.py +18 -24
  17. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_helpers.py +43 -97
  18. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_market_morpho_blue.py +1 -8
  19. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_info_golden.py +60 -23
  20. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_predates_getters.py +3 -1
  21. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_explorer.py +38 -18
  22. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_live.py +1 -5
  23. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_public_api.py +7 -0
  24. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/typed_client/consumer.py +12 -0
  25. ipor_fusion-4.0.0.dev106/src/ipor_fusion/cache.py +0 -86
  26. ipor_fusion-4.0.0.dev106/tests/test_cache.py +0 -99
  27. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.claude-plugin/marketplace.json +0 -0
  28. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.claude-plugin/plugin.json +0 -0
  29. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.env.example +0 -0
  30. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.gemini/settings.json +0 -0
  31. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.git-blame-ignore-revs +0 -0
  32. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/cd.yml +0 -0
  33. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/ci.yml +0 -0
  34. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/commit-lint.yml +0 -0
  35. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/notify-slack.yml +0 -0
  36. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/python-build.yml +0 -0
  37. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/python-publish.yml +0 -0
  38. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/release.yml +0 -0
  39. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.github/workflows/report-slack.yml +0 -0
  40. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.gitignore +0 -0
  41. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.mcp.json +0 -0
  42. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.pre-commit-config.yaml +0 -0
  43. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/.python-version +0 -0
  44. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CHANGELOG.md +0 -0
  45. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CLAUDE.md +0 -0
  46. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/CODEOWNERS +0 -0
  47. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/LICENSE +0 -0
  48. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/build.sh +0 -0
  49. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/CONTRIBUTING.md +0 -0
  50. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/README.md +0 -0
  51. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  52. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  53. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  54. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  55. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/external_state_margin_leg_base.py +0 -0
  56. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/examples/simple_aave_v3_supply_base.py +0 -0
  57. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/pytest.ini +0 -0
  58. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/scripts/commit-check.sh +0 -0
  59. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/scripts/crosschain_readiness.py +0 -0
  60. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/skills/ipor-deploy-vault/SKILL.md +0 -0
  61. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/about.py +0 -0
  62. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/chains.py +0 -0
  63. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/__init__.py +0 -0
  64. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  65. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/config_cmd.py +0 -0
  66. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/config_store.py +0 -0
  67. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/main.py +0 -0
  68. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/market_cmd.py +0 -0
  69. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/options.py +0 -0
  70. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  71. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  72. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_health.py +0 -0
  73. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  74. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/config/__init__.py +0 -0
  75. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/config/roles.py +0 -0
  76. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/__init__.py +0 -0
  77. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/access.py +0 -0
  78. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/address.py +0 -0
  79. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/contract.py +0 -0
  80. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/erc20.py +0 -0
  81. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/external_state_executor.py +0 -0
  82. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fee_manager.py +0 -0
  83. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fuse.py +0 -0
  84. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fuse_action.py +0 -0
  85. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/fusion_factory.py +0 -0
  86. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/logs.py +0 -0
  87. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/multicall.py +0 -0
  88. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/oracle.py +0 -0
  89. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/plasma_vault.py +0 -0
  90. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/rewards_manager.py +0 -0
  91. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/simulation.py +0 -0
  92. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  93. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/__init__.py +0 -0
  94. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  95. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  96. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  97. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  98. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  99. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  100. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  101. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  102. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  103. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/contracts.py +0 -0
  104. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/discovery.py +0 -0
  105. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/errors.py +0 -0
  106. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  107. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  108. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  109. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  110. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/lane.py +0 -0
  111. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/logs.py +0 -0
  112. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/messages.py +0 -0
  113. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/simulation.py +0 -0
  114. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  115. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  116. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  117. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  118. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  119. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/crosschain/transport.py +0 -0
  120. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/dependency_graph.py +0 -0
  121. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/entry_cli.py +0 -0
  122. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/entry_mcp.py +0 -0
  123. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/errors.py +0 -0
  124. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/field_docs.py +0 -0
  125. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/__init__.py +0 -0
  126. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  127. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/async_action.py +0 -0
  128. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/base.py +0 -0
  129. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  130. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/erc4626.py +0 -0
  131. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  132. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/events.py +0 -0
  133. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/external_state.py +0 -0
  134. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  135. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  136. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  137. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/hypercore.py +0 -0
  138. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  139. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/merkl.py +0 -0
  140. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/morpho.py +0 -0
  141. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  142. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  143. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/fuses/universal.py +0 -0
  144. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/__init__.py +0 -0
  145. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/architecture.md +0 -0
  146. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/glossary.md +0 -0
  147. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/invariants.md +0 -0
  148. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/guide/quickstart.md +0 -0
  149. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/market_ids.py +0 -0
  150. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/market_info.py +0 -0
  151. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/__init__.py +0 -0
  152. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/guide.py +0 -0
  153. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/mcp/models.py +0 -0
  154. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/morpho_api.py +0 -0
  155. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/py.typed +0 -0
  156. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/__init__.py +0 -0
  157. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/aave_v3.py +0 -0
  158. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/aave_v4.py +0 -0
  159. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/compound_v3.py +0 -0
  160. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/hypercore.py +0 -0
  161. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  162. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/lending_health.py +0 -0
  163. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/morpho.py +0 -0
  164. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  165. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  166. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  167. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/position_manager.py +0 -0
  168. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  169. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  170. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/readers/unpriceable.py +0 -0
  171. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/substrates.py +0 -0
  172. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/types.py +0 -0
  173. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/validation.py +0 -0
  174. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_erc20.py +0 -0
  175. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_managers.py +0 -0
  176. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_positions.py +0 -0
  177. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/src/ipor_fusion/vault_reads.py +0 -0
  178. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/UniswapV3UniversalRouter.py +0 -0
  179. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain.py +0 -0
  180. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain_lifecycle.py +0 -0
  181. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_crosschain_recovery.py +0 -0
  182. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_euler_v2.py +0 -0
  183. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_foundry.py +0 -0
  184. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_guide.py +0 -0
  185. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_hypercore_model.py +0 -0
  186. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_hypercore_shadow.py +0 -0
  187. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_multicall.py +0 -0
  188. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_simulate.py +0 -0
  189. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/_validation.py +0 -0
  190. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/addresses.py +0 -0
  191. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/conftest.py +0 -0
  192. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/constants.py +0 -0
  193. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  194. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/hypercore_market55.json +0 -0
  195. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/clean.json +0 -0
  196. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  197. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/rich.json +0 -0
  198. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  199. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_about.py +0 -0
  200. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_access_manager.py +0 -0
  201. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_chains.py +0 -0
  202. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_changelog.py +0 -0
  203. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_config_store.py +0 -0
  204. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_hypercore.py +0 -0
  205. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_lending_health.py +0 -0
  206. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_oracle_mapping.py +0 -0
  207. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_pending_requests_scan.py +0 -0
  208. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_role_accounts.py +0 -0
  209. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_cli_vault_validate.py +0 -0
  210. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_contract_call.py +0 -0
  211. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_contract_encoder.py +0 -0
  212. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_events.py +0 -0
  213. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_indexer.py +0 -0
  214. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_ccip_quote.py +0 -0
  215. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_chainlink.py +0 -0
  216. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_contracts.py +0 -0
  217. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_encoding.py +0 -0
  218. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_errors.py +0 -0
  219. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_lane.py +0 -0
  220. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_layering.py +0 -0
  221. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_readiness.py +0 -0
  222. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_readiness_report.py +0 -0
  223. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_crosschain_transport.py +0 -0
  224. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_erc20.py +0 -0
  225. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_error_hierarchy.py +0 -0
  226. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_composed_hypercore.py +0 -0
  227. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_crosschain.py +0 -0
  228. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  229. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_examples_vaults.py +0 -0
  230. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_external_state_events.py +0 -0
  231. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_external_state_executor.py +0 -0
  232. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fee_manager.py +0 -0
  233. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fuse_encoding.py +0 -0
  234. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_fusion_instance.py +0 -0
  235. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_guide.py +0 -0
  236. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_deployment.py +0 -0
  237. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_events.py +0 -0
  238. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_flow.py +0 -0
  239. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_outcome.py +0 -0
  240. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_readers.py +0 -0
  241. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_hypercore_simulation.py +0 -0
  242. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_logs.py +0 -0
  243. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_hypercore.py +0 -0
  244. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_models.py +0 -0
  245. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_server.py +0 -0
  246. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_mcp_vault_validate.py +0 -0
  247. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_multicall.py +0 -0
  248. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_oracle.py +0 -0
  249. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_oracle_mapping.py +0 -0
  250. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_plasma_vault.py +0 -0
  251. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_reader_encoding.py +0 -0
  252. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_readiness_selection.py +0 -0
  253. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_revert_decoding.py +0 -0
  254. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_rewards_manager.py +0 -0
  255. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_roles.py +0 -0
  256. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_sdk_layering.py +0 -0
  257. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  258. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  259. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  260. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_canary.py +0 -0
  261. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_hypercore.py +0 -0
  262. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  263. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  264. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  268. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_arbitrum.py +0 -0
  270. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_examples_crosschain.py +0 -0
  272. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_external_state_executor_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  275. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  276. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_morpho_blue.py +0 -0
  277. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v3.py +0 -0
  278. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  279. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  280. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  281. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  282. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_readers_integration.py +0 -0
  283. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_release_funds.py +0 -0
  284. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_swap_on_base.py +0 -0
  285. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  286. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_solidity_compiler.py +0 -0
  287. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_solidity_mirrors.py +0 -0
  288. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_substrates.py +0 -0
  289. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_types_and_events.py +0 -0
  290. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_unpriceable_tokens.py +0 -0
  291. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_validation.py +0 -0
  292. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_dep_graph.py +0 -0
  293. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_info_onchain_base.py +0 -0
  294. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  295. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_vault_simulator_blocks.py +0 -0
  296. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_web3_context.py +0 -0
  297. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/test_withdraw_manager.py +0 -0
  298. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/tests/typed_client/rejected.py +0 -0
  299. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev110}/uv.lock +0 -0
@@ -158,7 +158,7 @@ the lifecycle matrix follows that.
158
158
  subpackage (`core`, `config`, `fuses`, `readers`, `crosschain`,
159
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
- public modules (`about`, `chains`, `dependency_graph`, `errors`, `explorer`,
161
+ public modules (`about`, `cache`, `chains`, `dependency_graph`, `errors`, `explorer`,
162
162
  `field_docs`, `market_ids`, `market_info`, `morpho_api`, `substrates`, `types`,
163
163
  `validation`, `vault_data`, `vault_info`) stay importable for the names they
164
164
  define; `cli`, `mcp` and every other module are internal. Examples, the guide
@@ -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.
@@ -388,8 +394,11 @@ byte, e.g. Ebisu ZAPPER/REGISTRY).
388
394
  role-accounts, oracle-mapping). `vault info` runs independent fetch pipelines
389
395
  (vault/asset, fees, WithdrawManager, markets, roles) concurrently on a
390
396
  `ThreadPoolExecutor`, each a short chain of `Multicall3` batches; keep new
391
- reads inside a batch rather than adding per-call round trips. Contract names
392
- 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.
393
402
 
394
403
  Every CLI command has an MCP tool (`fusion changelog` maps to `server_info`;
395
404
  the rest share names). Read-only: `vault_info`, `vault_validate`,
@@ -334,6 +334,66 @@ argument (`chain_id=`) for another chain. An external signer still needs the rea
334
334
  target address (`call.to`) and builds the chain-qualified transaction itself, or
335
335
  gets it from `call.build_transaction(ctx)`.
336
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. The CLI's text and JSON output name contracts through the
355
+ context's cache.
356
+ - `build_vault_info_json(data)` without `resolve_name` names no contract (every
357
+ `contract` is `"?"`) instead of reading names from the on-disk cache; pass
358
+ `resolve_name=partial(get_contract_name, chain_id, api_key=..., cache=ctx.cache)`
359
+ for the CLI's document.
360
+ - `ipor_fusion.cache` is a public module now and exports `ContractCache`,
361
+ `FileContractCache`, `NullCache`, `CacheKey`, `CacheField`, `cache_key` and
362
+ `default_contract_cache` (also from `ipor_fusion`). Its 3.x functions and constants
363
+ are gone: `load_contract_cache`, `save_contract_cache`, `update_contract_cache`,
364
+ `load_deployment_cache`, `update_deployment_cache`, `CACHE_DIR`, `CACHE_FILE` and
365
+ `DEPLOYMENT_CACHE_FILE`. `cache_key(chain_id, address, field)` returns a `CacheKey`
366
+ and raises `ValueError` for an address that is not 20 bytes of hex, so
367
+ `get_contract_name` does too.
368
+
369
+ **File format.** One file, keys `<chain_id>:<lowercase address>:<field>` (`name`,
370
+ `symbol`, `decimals`, `deployment_block`, `deployment_timestamp`), string values. On
371
+ load, a 3.x contract name (`<chain_id>:<address>`) is kept as that chain's `name`;
372
+ 3.x token entries (`symbol:<address>`, `decimals:<address>`) name no chain and are
373
+ discarded, so symbols and decimals are read again once per chain. `deployment_cache.json`
374
+ is no longer read: a vault's deployment block is resolved again (it needs an
375
+ Etherscan key) and the old file can be deleted.
376
+
377
+ **Behavior.**
378
+
379
+ - The same address on two chains has two entries: a token's symbol or decimals read
380
+ on one chain is never served for another.
381
+ - Absence is not cached: `resolve_token_symbol` still returns `"no contract"` for an
382
+ address without code, but no longer stores it, so a token deployed later (or seen at
383
+ a later block) resolves.
384
+ - A write merges with the file as it is on disk and replaces it atomically (a
385
+ temporary file in the same directory, then `os.replace`), so a crash or two CLI/MCP
386
+ processes writing at once never leave a torn file; one of two concurrent writers may
387
+ lose its newest entry, which is read again later.
388
+ - A cache never fails a read: a corrupt or unreadable file reads as empty (logged
389
+ warning from `ipor_fusion.cache`) and is rewritten by the next write; an unwritable
390
+ directory (read-only file system) keeps the entries in memory for the process; a
391
+ custom cache that raises is treated as a miss, and so is a stored number that does
392
+ not parse.
393
+ - `fetch_deployment_info` returns `rpc-fetch-failed` only when the creation
394
+ transaction or its block cannot be read; a cache failure is no longer reported as
395
+ one (the deployment block is returned).
396
+
337
397
  ## Behavior changes
338
398
 
339
399
  - Two fuses are equal (and hash alike) only when they have the same class and the
@@ -371,9 +431,8 @@ gets it from `call.build_transaction(ctx)`.
371
431
  - `TransactionError.tx_hash` is `0x`-prefixed.
372
432
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
373
433
  a value that is not an address.
374
- - The on-disk caches (`~/.cache/ipor-fusion/`) key addresses lowercased; older entries
375
- are rekeyed on load, so a contract name cached for one spelling now also resolves
376
- for the other.
434
+ - The contract cache keys every entry by chain and lowercase address and no longer
435
+ caches an absence (see [Contract cache](#contract-cache)).
377
436
 
378
437
  ## Errors
379
438
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev106
3
+ Version: 4.0.0.dev110
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
@@ -381,11 +381,11 @@ release; adding one does not.
381
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
@@ -333,11 +333,11 @@ release; adding one does not.
333
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.dev106"
3
+ version = "4.0.0.dev110"
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,
@@ -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)
@@ -85,10 +85,11 @@ def _print_vault_info(
85
85
  data.deployment_error,
86
86
  ) = fetch_deployment_info(ctx, data.vault, api_key=api_key)
87
87
 
88
+ resolve_name = partial(
89
+ get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
90
+ )
88
91
  if json_output:
89
- result = build_vault_info_json(
90
- data, resolve_name=partial(get_contract_name, chain_id, api_key=api_key)
91
- )
92
+ result = build_vault_info_json(data, resolve_name=resolve_name)
92
93
  click.echo(json.dumps(result, indent=2))
93
94
  return
94
95
 
@@ -102,7 +103,7 @@ def _print_vault_info(
102
103
 
103
104
  _print_role_accounts(_role_accounts_json(data))
104
105
 
105
- _print_markets(data, api_key)
106
+ _print_markets(data, resolve_name)
106
107
 
107
108
  validation = validate_vault_state(data)
108
109
  click.echo("ERC20 Balances (vault holdings):")
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Sequence
3
+ from collections.abc import Callable, Sequence
4
4
  from concurrent.futures import Future, ThreadPoolExecutor
5
5
  from typing import Any
6
6
 
@@ -13,12 +13,11 @@ from ipor_fusion.cli.vault_rendering import (
13
13
  _print_table,
14
14
  _substrate_details,
15
15
  )
16
- from ipor_fusion.explorer import get_contract_name
17
16
  from ipor_fusion.substrates import (
18
17
  decode_substrate,
19
18
  format_market_label,
20
19
  )
21
- from ipor_fusion.types import ChainId, FuseAddress, MarketId, parse_hex_address
20
+ from ipor_fusion.types import FuseAddress, MarketId, parse_hex_address
22
21
  from ipor_fusion.vault_data import (
23
22
  GET_MARKET_SUBSTRATES,
24
23
  VaultData,
@@ -27,6 +26,10 @@ from ipor_fusion.vault_info import (
27
26
  _CAPABILITY_BALANCE_FUSES,
28
27
  )
29
28
 
29
+ # Address to contract name, "" when unknown (`get_contract_name` bound to the
30
+ # chain, API key and the context's cache).
31
+ NameResolver = Callable[[HexAddress], str]
32
+
30
33
 
31
34
  def _print_lending_health(data: VaultData) -> None: # noqa: C901
32
35
  lh = data.lending_health
@@ -154,16 +157,14 @@ def _print_health_lines(market: Any, indent: str) -> None:
154
157
  click.secho(f"{indent}Status: {status}", fg=color, bold=bold)
155
158
 
156
159
 
157
- def _print_markets(data: VaultData, api_key: str | None) -> None:
160
+ def _print_markets(data: VaultData, resolve_name: NameResolver) -> None:
158
161
  """Fuses, balance fuses, dependency graph and substrates."""
159
- chain_id = data.chain_id
160
162
  _print_fuse_section(
161
163
  "Fuses",
162
164
  data.fuses,
163
165
  data.fuse_markets,
164
166
  data.market_substrates,
165
- chain_id,
166
- api_key,
167
+ resolve_name,
167
168
  )
168
169
  click.echo()
169
170
 
@@ -172,18 +173,17 @@ def _print_markets(data: VaultData, api_key: str | None) -> None:
172
173
  data.instant_fuses,
173
174
  data.fuse_markets,
174
175
  data.market_substrates,
175
- chain_id,
176
- api_key,
176
+ resolve_name,
177
177
  )
178
178
  click.echo()
179
179
 
180
- _print_balance_fuses_table(data, api_key)
180
+ _print_balance_fuses_table(data, resolve_name)
181
181
  click.echo()
182
182
 
183
183
  _print_dependency_graph(data)
184
184
 
185
185
  click.echo("Substrates per Market:")
186
- _print_market_substrates(data, api_key)
186
+ _print_market_substrates(data, resolve_name)
187
187
  click.echo()
188
188
 
189
189
 
@@ -235,8 +235,7 @@ def _print_fuse_section(
235
235
  fuses: Sequence[FuseAddress],
236
236
  fuse_markets: dict[FuseAddress, MarketId] | None,
237
237
  market_substrates: dict[MarketId, list[bytes]] | None,
238
- chain_id: ChainId,
239
- api_key: str | None,
238
+ resolve_name: NameResolver,
240
239
  ) -> None:
241
240
  """Render a fuse section (regular or instant-withdrawal) with a unified
242
241
  layout.
@@ -262,10 +261,7 @@ def _print_fuse_section(
262
261
  return
263
262
 
264
263
  with ThreadPoolExecutor() as pool:
265
- name_futs = {
266
- addr: pool.submit(get_contract_name, chain_id, addr, api_key)
267
- for addr in counts
268
- }
264
+ name_futs = {addr: pool.submit(resolve_name, addr) for addr in counts}
269
265
  rows: list[tuple[str, ...]] = []
270
266
  for idx, addr in enumerate(counts, 1):
271
267
  market_label = "?"
@@ -285,7 +281,7 @@ def _print_fuse_section(
285
281
  _print_table(("#", "Address", "Contract", "Market", "Substrates"), rows)
286
282
 
287
283
 
288
- def _print_balance_fuses_table(data: VaultData, api_key: str | None) -> None:
284
+ def _print_balance_fuses_table(data: VaultData, resolve_name: NameResolver) -> None:
289
285
  balance_fuses = data.balance_fuses
290
286
  decimals = data.asset_decimals
291
287
  asset_symbol = data.asset_symbol
@@ -294,9 +290,7 @@ def _print_balance_fuses_table(data: VaultData, api_key: str | None) -> None:
294
290
  futures: list[tuple[int, MarketId, str, Future]] = []
295
291
  for idx, balance_fuse in enumerate(balance_fuses, 1):
296
292
  market_id_str = format_market_label(balance_fuse.market_id)
297
- f_contract = pool.submit(
298
- get_contract_name, data.chain_id, balance_fuse.fuse, api_key
299
- )
293
+ f_contract = pool.submit(resolve_name, balance_fuse.fuse)
300
294
  futures.append((idx, balance_fuse.market_id, market_id_str, f_contract))
301
295
 
302
296
  venue_rows: list[tuple[str, ...]] = []
@@ -328,18 +322,18 @@ def _print_balance_fuses_table(data: VaultData, api_key: str | None) -> None:
328
322
  _print_table(header, [(str(i), *row) for i, row in enumerate(zero_rows, 1)])
329
323
 
330
324
 
331
- def _print_market_substrates(data: VaultData, api_key: str | None) -> set[str]:
325
+ def _print_market_substrates(data: VaultData, resolve_name: NameResolver) -> set[str]:
332
326
  if GET_MARKET_SUBSTRATES in data.unimplemented_getters:
333
327
  click.secho(
334
328
  f" (unavailable: the vault predates {GET_MARKET_SUBSTRATES})",
335
329
  fg="yellow",
336
330
  )
337
331
  return set()
338
- return _print_substrates(data, api_key)
332
+ return _print_substrates(data, resolve_name)
339
333
 
340
334
 
341
335
  def _print_substrates( # noqa: C901
342
- data: VaultData, api_key: str | None
336
+ data: VaultData, resolve_name: NameResolver
343
337
  ) -> set[str]:
344
338
  substrates_by_market = data.market_substrates or {}
345
339
  balance_fuses = data.balance_fuses
@@ -364,8 +358,7 @@ def _print_substrates( # noqa: C901
364
358
  resolved_symbols = data.token_symbols
365
359
  with ThreadPoolExecutor() as pool:
366
360
  contract_futs = {
367
- addr: pool.submit(get_contract_name, data.chain_id, addr, api_key)
368
- for addr in all_addresses
361
+ addr: pool.submit(resolve_name, addr) for addr in all_addresses
369
362
  }
370
363
  resolved_contracts = {addr: fut.result() for addr, fut in contract_futs.items()}
371
364
 
@@ -18,6 +18,7 @@ from web3.providers.rpc.utils import (
18
18
  )
19
19
  from web3.types import BlockIdentifier, LogReceipt, TxParams, TxReceipt, Wei
20
20
 
21
+ from ipor_fusion.cache import ContractCache, default_contract_cache
21
22
  from ipor_fusion.chains import GET_LOGS_RANGE_HINTS
22
23
  from ipor_fusion.core.logs import (
23
24
  DEFAULT_LOG_SCAN_MAX_REQUESTS,
@@ -81,6 +82,7 @@ class Web3Context:
81
82
  log_scan_timeout_s: float = DEFAULT_LOG_SCAN_TIMEOUT_S,
82
83
  log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
83
84
  log_source: LogSource | None = None,
85
+ cache: ContractCache | None = None,
84
86
  ):
85
87
  self._web3 = web3
86
88
  self._chain_id = chain_id
@@ -90,6 +92,8 @@ class Web3Context:
90
92
  self.log_scan_max_requests = log_scan_max_requests
91
93
  # Asked first by `get_logs`; None scans the RPC only.
92
94
  self.log_source = log_source
95
+ # None: the process-wide file cache (`cache.default_contract_cache`).
96
+ self._cache = cache
93
97
  self._private_key = private_key
94
98
  self._gas_multiplier = gas_multiplier
95
99
  self._default_block: BlockIdentifier = "latest"
@@ -109,6 +113,14 @@ class Web3Context:
109
113
  def chain_id(self) -> ChainId:
110
114
  return self._chain_id
111
115
 
116
+ @property
117
+ def cache(self) -> ContractCache:
118
+ """Where the SDK keeps immutable contract facts read through this
119
+ context (token symbols and decimals, contract names, deployment
120
+ blocks): the ``cache=`` passed in, else the file cache under
121
+ ``$XDG_CACHE_HOME/ipor-fusion/``. Pass `NullCache()` to keep nothing."""
122
+ return self._cache if self._cache is not None else default_contract_cache()
123
+
112
124
  @property
113
125
  def default_block(self) -> BlockIdentifier:
114
126
  return self._default_block
@@ -119,7 +131,7 @@ class Web3Context:
119
131
 
120
132
  def at_block(self, block: BlockIdentifier) -> Web3Context:
121
133
  """A copy of this context whose reads default to ``block``: same
122
- connection, signer, log source and scan bounds. Pinning a copy leaves
134
+ connection, signer, log source, cache and scan bounds. Pinning a copy leaves
123
135
  this context (and anything sharing it across threads) untouched."""
124
136
  pinned = copy.copy(self)
125
137
  pinned._default_block = block
@@ -140,6 +152,7 @@ class Web3Context:
140
152
  log_scan_timeout_s: float = DEFAULT_LOG_SCAN_TIMEOUT_S,
141
153
  log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
142
154
  log_source: LogSource | None = None,
155
+ cache: ContractCache | None = None,
143
156
  ) -> Web3Context:
144
157
  web3 = Web3(
145
158
  Web3.HTTPProvider(
@@ -168,6 +181,7 @@ class Web3Context:
168
181
  log_scan_timeout_s=log_scan_timeout_s,
169
182
  log_scan_max_requests=log_scan_max_requests,
170
183
  log_source=log_source,
184
+ cache=cache,
171
185
  )
172
186
 
173
187
  def call(