ipor-fusion 4.0.0.dev106__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 (299) hide show
  1. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/AGENTS.md +12 -3
  2. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/MIGRATION_4.md +57 -3
  3. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/PKG-INFO +11 -3
  4. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/README.md +10 -2
  5. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/pyproject.toml +1 -1
  6. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/__init__.py +16 -0
  7. ipor_fusion-4.0.0.dev108/src/ipor_fusion/cache.py +204 -0
  8. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_info_view.py +4 -1
  9. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/context.py +15 -1
  10. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/explorer.py +23 -11
  11. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/server.py +4 -1
  12. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_data.py +60 -47
  13. ipor_fusion-4.0.0.dev108/tests/test_cache.py +316 -0
  14. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_commands.py +12 -6
  15. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_helpers.py +15 -45
  16. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_market_morpho_blue.py +1 -8
  17. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_info_golden.py +7 -11
  18. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_explorer.py +38 -18
  19. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_live.py +1 -5
  20. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_public_api.py +7 -0
  21. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/typed_client/consumer.py +12 -0
  22. ipor_fusion-4.0.0.dev106/src/ipor_fusion/cache.py +0 -86
  23. ipor_fusion-4.0.0.dev106/tests/test_cache.py +0 -99
  24. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.claude-plugin/marketplace.json +0 -0
  25. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.claude-plugin/plugin.json +0 -0
  26. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.env.example +0 -0
  27. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.gemini/settings.json +0 -0
  28. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.git-blame-ignore-revs +0 -0
  29. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/cd.yml +0 -0
  30. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/ci.yml +0 -0
  31. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/commit-lint.yml +0 -0
  32. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/notify-slack.yml +0 -0
  33. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-build.yml +0 -0
  34. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/python-publish.yml +0 -0
  35. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/release.yml +0 -0
  36. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.github/workflows/report-slack.yml +0 -0
  37. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.gitignore +0 -0
  38. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.mcp.json +0 -0
  39. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.pre-commit-config.yaml +0 -0
  40. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/.python-version +0 -0
  41. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CHANGELOG.md +0 -0
  42. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CLAUDE.md +0 -0
  43. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/CODEOWNERS +0 -0
  44. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/LICENSE +0 -0
  45. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/build.sh +0 -0
  46. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/CONTRIBUTING.md +0 -0
  47. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/README.md +0 -0
  48. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  49. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  50. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  51. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  52. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/external_state_margin_leg_base.py +0 -0
  53. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/examples/simple_aave_v3_supply_base.py +0 -0
  54. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/pytest.ini +0 -0
  55. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/scripts/commit-check.sh +0 -0
  56. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/scripts/crosschain_readiness.py +0 -0
  57. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/skills/ipor-deploy-vault/SKILL.md +0 -0
  58. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/about.py +0 -0
  59. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/chains.py +0 -0
  60. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/__init__.py +0 -0
  61. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  62. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_cmd.py +0 -0
  63. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/config_store.py +0 -0
  64. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/main.py +0 -0
  65. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/market_cmd.py +0 -0
  66. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/options.py +0 -0
  67. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  68. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  69. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_health.py +0 -0
  70. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  71. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  72. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/__init__.py +0 -0
  73. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/config/roles.py +0 -0
  74. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/__init__.py +0 -0
  75. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/access.py +0 -0
  76. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/address.py +0 -0
  77. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/contract.py +0 -0
  78. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/erc20.py +0 -0
  79. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/external_state_executor.py +0 -0
  80. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fee_manager.py +0 -0
  81. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse.py +0 -0
  82. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fuse_action.py +0 -0
  83. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/fusion_factory.py +0 -0
  84. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/logs.py +0 -0
  85. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/multicall.py +0 -0
  86. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/oracle.py +0 -0
  87. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/plasma_vault.py +0 -0
  88. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/rewards_manager.py +0 -0
  89. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/simulation.py +0 -0
  90. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  91. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/__init__.py +0 -0
  92. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  93. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  94. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  95. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  96. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  97. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  98. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  99. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  100. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  101. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/contracts.py +0 -0
  102. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/discovery.py +0 -0
  103. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/errors.py +0 -0
  104. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  105. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  106. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  107. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  108. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/lane.py +0 -0
  109. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/logs.py +0 -0
  110. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/messages.py +0 -0
  111. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/simulation.py +0 -0
  112. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  113. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  114. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  115. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  116. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  117. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/crosschain/transport.py +0 -0
  118. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/dependency_graph.py +0 -0
  119. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_cli.py +0 -0
  120. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/entry_mcp.py +0 -0
  121. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/errors.py +0 -0
  122. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/field_docs.py +0 -0
  123. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/__init__.py +0 -0
  124. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  125. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/async_action.py +0 -0
  126. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/base.py +0 -0
  127. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  128. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/erc4626.py +0 -0
  129. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  130. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/events.py +0 -0
  131. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state.py +0 -0
  132. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  133. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  134. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  135. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore.py +0 -0
  136. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  137. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/merkl.py +0 -0
  138. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/morpho.py +0 -0
  139. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  140. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  141. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/fuses/universal.py +0 -0
  142. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/__init__.py +0 -0
  143. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/architecture.md +0 -0
  144. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/glossary.md +0 -0
  145. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/invariants.md +0 -0
  146. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/guide/quickstart.md +0 -0
  147. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_ids.py +0 -0
  148. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/market_info.py +0 -0
  149. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/__init__.py +0 -0
  150. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/guide.py +0 -0
  151. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/mcp/models.py +0 -0
  152. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/morpho_api.py +0 -0
  153. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/py.typed +0 -0
  154. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/__init__.py +0 -0
  155. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v3.py +0 -0
  156. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/aave_v4.py +0 -0
  157. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/compound_v3.py +0 -0
  158. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore.py +0 -0
  159. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  160. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/lending_health.py +0 -0
  161. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/morpho.py +0 -0
  162. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  163. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  164. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  165. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/position_manager.py +0 -0
  166. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  167. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  168. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/readers/unpriceable.py +0 -0
  169. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/substrates.py +0 -0
  170. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/types.py +0 -0
  171. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/validation.py +0 -0
  172. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_erc20.py +0 -0
  173. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_info.py +0 -0
  174. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_managers.py +0 -0
  175. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_positions.py +0 -0
  176. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/src/ipor_fusion/vault_reads.py +0 -0
  177. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/UniswapV3UniversalRouter.py +0 -0
  178. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain.py +0 -0
  179. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_lifecycle.py +0 -0
  180. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_crosschain_recovery.py +0 -0
  181. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_euler_v2.py +0 -0
  182. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_foundry.py +0 -0
  183. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_guide.py +0 -0
  184. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_model.py +0 -0
  185. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_hypercore_shadow.py +0 -0
  186. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_multicall.py +0 -0
  187. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_simulate.py +0 -0
  188. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/_validation.py +0 -0
  189. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/addresses.py +0 -0
  190. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/conftest.py +0 -0
  191. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/constants.py +0 -0
  192. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  193. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/hypercore_market55.json +0 -0
  194. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.json +0 -0
  195. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  196. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.json +0 -0
  197. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  198. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_about.py +0 -0
  199. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_access_manager.py +0 -0
  200. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_chains.py +0 -0
  201. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_changelog.py +0 -0
  202. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_config_store.py +0 -0
  203. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_hypercore.py +0 -0
  204. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_lending_health.py +0 -0
  205. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_oracle_mapping.py +0 -0
  206. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_pending_requests_scan.py +0 -0
  207. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_role_accounts.py +0 -0
  208. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_predates_getters.py +0 -0
  209. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_cli_vault_validate.py +0 -0
  210. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_contract_call.py +0 -0
  211. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_contract_encoder.py +0 -0
  212. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_events.py +0 -0
  213. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_indexer.py +0 -0
  214. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_ccip_quote.py +0 -0
  215. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_chainlink.py +0 -0
  216. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_contracts.py +0 -0
  217. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_encoding.py +0 -0
  218. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_errors.py +0 -0
  219. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_lane.py +0 -0
  220. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_layering.py +0 -0
  221. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness.py +0 -0
  222. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_readiness_report.py +0 -0
  223. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_crosschain_transport.py +0 -0
  224. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_erc20.py +0 -0
  225. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_error_hierarchy.py +0 -0
  226. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_composed_hypercore.py +0 -0
  227. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_crosschain.py +0 -0
  228. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  229. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_examples_vaults.py +0 -0
  230. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_events.py +0 -0
  231. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_external_state_executor.py +0 -0
  232. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fee_manager.py +0 -0
  233. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fuse_encoding.py +0 -0
  234. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_fusion_instance.py +0 -0
  235. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_guide.py +0 -0
  236. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_deployment.py +0 -0
  237. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_events.py +0 -0
  238. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_flow.py +0 -0
  239. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_outcome.py +0 -0
  240. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_readers.py +0 -0
  241. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_hypercore_simulation.py +0 -0
  242. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_logs.py +0 -0
  243. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_hypercore.py +0 -0
  244. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_models.py +0 -0
  245. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_server.py +0 -0
  246. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_mcp_vault_validate.py +0 -0
  247. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_multicall.py +0 -0
  248. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_oracle.py +0 -0
  249. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_oracle_mapping.py +0 -0
  250. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_plasma_vault.py +0 -0
  251. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_reader_encoding.py +0 -0
  252. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_readiness_selection.py +0 -0
  253. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_revert_decoding.py +0 -0
  254. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_rewards_manager.py +0 -0
  255. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_roles.py +0 -0
  256. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_sdk_layering.py +0 -0
  257. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  258. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  259. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  260. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_canary.py +0 -0
  261. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hypercore.py +0 -0
  262. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  263. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  264. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  268. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_arbitrum.py +0 -0
  270. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_examples_crosschain.py +0 -0
  272. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_external_state_executor_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  275. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  276. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_morpho_blue.py +0 -0
  277. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v3.py +0 -0
  278. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  279. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  280. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  281. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  282. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_readers_integration.py +0 -0
  283. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_release_funds.py +0 -0
  284. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_swap_on_base.py +0 -0
  285. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  286. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_compiler.py +0 -0
  287. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_solidity_mirrors.py +0 -0
  288. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_substrates.py +0 -0
  289. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_types_and_events.py +0 -0
  290. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_unpriceable_tokens.py +0 -0
  291. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_validation.py +0 -0
  292. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_dep_graph.py +0 -0
  293. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base.py +0 -0
  294. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  295. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_vault_simulator_blocks.py +0 -0
  296. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_web3_context.py +0 -0
  297. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/test_withdraw_manager.py +0 -0
  298. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/tests/typed_client/rejected.py +0 -0
  299. {ipor_fusion-4.0.0.dev106 → ipor_fusion-4.0.0.dev108}/uv.lock +0 -0
@@ -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,61 @@ 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. `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
+
337
392
  ## Behavior changes
338
393
 
339
394
  - Two fuses are equal (and hash alike) only when they have the same class and the
@@ -371,9 +426,8 @@ gets it from `call.build_transaction(ctx)`.
371
426
  - `TransactionError.tx_hash` is `0x`-prefixed.
372
427
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
373
428
  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.
429
+ - The contract cache keys every entry by chain and lowercase address and no longer
430
+ caches an absence (see [Contract cache](#contract-cache)).
377
431
 
378
432
  ## Errors
379
433
 
@@ -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.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
@@ -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.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,
@@ -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)
@@ -87,7 +87,10 @@ def _print_vault_info(
87
87
 
88
88
  if json_output:
89
89
  result = build_vault_info_json(
90
- data, resolve_name=partial(get_contract_name, chain_id, api_key=api_key)
90
+ data,
91
+ resolve_name=partial(
92
+ get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
93
+ ),
91
94
  )
92
95
  click.echo(json.dumps(result, indent=2))
93
96
  return
@@ -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(
@@ -1,6 +1,6 @@
1
- """Etherscan V2 client: contract names (cached on disk) and contract creation
2
- transactions. Every lookup needs an API key; without one, names come from the
3
- cache only."""
1
+ """Etherscan V2 client: contract names (cached in a `ContractCache`) and
2
+ contract creation transactions. Every lookup needs an API key; without one,
3
+ names come from the cache only."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -12,7 +12,13 @@ from urllib.request import urlopen
12
12
 
13
13
  from eth_typing import HexAddress
14
14
 
15
- from ipor_fusion.cache import cache_key, load_contract_cache, update_contract_cache
15
+ from ipor_fusion.cache import (
16
+ ContractCache,
17
+ cache_get,
18
+ cache_key,
19
+ cache_put,
20
+ default_contract_cache,
21
+ )
16
22
  from ipor_fusion.types import ChainId, TxHash, parse_tx_hash
17
23
 
18
24
  ETHERSCAN_V2_URL = "https://api.etherscan.io/v2/api"
@@ -43,16 +49,22 @@ _etherscan_limiter = _RateLimiter(2.5)
43
49
 
44
50
 
45
51
  def get_contract_name(
46
- chain_id: ChainId, address: HexAddress, api_key: str | None = None
52
+ chain_id: ChainId,
53
+ address: HexAddress,
54
+ api_key: str | None = None,
55
+ *,
56
+ cache: ContractCache | None = None,
47
57
  ) -> str:
48
- cache = load_contract_cache()
49
- key = cache_key(chain_id, address)
50
-
51
- if cached := cache.get(key):
58
+ """The verified contract name at ``address``, ``""`` when unknown. Read from
59
+ ``cache`` first (default: the file cache, `default_contract_cache`; pass a
60
+ context's ``ctx.cache`` to share its policy), else from Etherscan when
61
+ ``api_key`` is set; a name found there is cached, its absence is not."""
62
+ store = default_contract_cache() if cache is None else cache
63
+ key = cache_key(chain_id, address, "name")
64
+ if cached := cache_get(store, [key]).get(key):
52
65
  return cached
53
-
54
66
  if name := _fetch_contract_name(chain_id, address, api_key):
55
- update_contract_cache(key, name)
67
+ cache_put(store, {key: name})
56
68
  return name
57
69
  return ""
58
70
 
@@ -210,7 +210,10 @@ def vault_info(
210
210
  ) = fetch_deployment_info(ctx, checksum, api_key=api_key)
211
211
 
212
212
  result = build_vault_info_json(
213
- data, resolve_name=partial(get_contract_name, chain_id, api_key=api_key)
213
+ data,
214
+ resolve_name=partial(
215
+ get_contract_name, chain_id, api_key=api_key, cache=ctx.cache
216
+ ),
214
217
  )
215
218
  return VaultInfoResponse.model_validate(result)
216
219