ipor-fusion 4.0.0.dev114__tar.gz → 4.0.0.dev116__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 (297) hide show
  1. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/AGENTS.md +3 -1
  2. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/MIGRATION_4.md +6 -0
  3. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/PKG-INFO +1 -1
  4. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/pyproject.toml +1 -1
  5. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/context.py +43 -3
  6. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_erc20.py +28 -32
  7. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/conftest.py +1 -0
  8. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_helpers.py +2 -0
  9. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_web3_context.py +43 -1
  10. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.claude-plugin/marketplace.json +0 -0
  11. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.claude-plugin/plugin.json +0 -0
  12. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.env.example +0 -0
  13. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.gemini/settings.json +0 -0
  14. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.git-blame-ignore-revs +0 -0
  15. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/cd.yml +0 -0
  16. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/ci.yml +0 -0
  17. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/commit-lint.yml +0 -0
  18. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/notify-slack.yml +0 -0
  19. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/python-build.yml +0 -0
  20. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/python-publish.yml +0 -0
  21. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/release.yml +0 -0
  22. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.github/workflows/report-slack.yml +0 -0
  23. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.gitignore +0 -0
  24. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.mcp.json +0 -0
  25. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.pre-commit-config.yaml +0 -0
  26. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/.python-version +0 -0
  27. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/CHANGELOG.md +0 -0
  28. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/CLAUDE.md +0 -0
  29. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/CODEOWNERS +0 -0
  30. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/LICENSE +0 -0
  31. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/README.md +0 -0
  32. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/build.sh +0 -0
  33. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/CONTRIBUTING.md +0 -0
  34. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/README.md +0 -0
  35. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  36. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  37. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  38. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  39. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/external_state_margin_leg_base.py +0 -0
  40. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/examples/simple_aave_v3_supply_base.py +0 -0
  41. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/pytest.ini +0 -0
  42. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/scripts/commit-check.sh +0 -0
  43. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/scripts/crosschain_readiness.py +0 -0
  44. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/skills/ipor-deploy-vault/SKILL.md +0 -0
  45. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/__init__.py +0 -0
  46. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/about.py +0 -0
  47. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cache.py +0 -0
  48. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/chains.py +0 -0
  49. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/__init__.py +0 -0
  50. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  51. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/config_cmd.py +0 -0
  52. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/config_store.py +0 -0
  53. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/main.py +0 -0
  54. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/market_cmd.py +0 -0
  55. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/options.py +0 -0
  56. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  57. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  58. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/vault_health.py +0 -0
  59. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  60. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  61. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  62. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/config/__init__.py +0 -0
  63. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/config/roles.py +0 -0
  64. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/__init__.py +0 -0
  65. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/access.py +0 -0
  66. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/address.py +0 -0
  67. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/contract.py +0 -0
  68. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/erc20.py +0 -0
  69. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/external_state_executor.py +0 -0
  70. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/fee_manager.py +0 -0
  71. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/fuse.py +0 -0
  72. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/fuse_action.py +0 -0
  73. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/fusion_factory.py +0 -0
  74. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/logs.py +0 -0
  75. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/multicall.py +0 -0
  76. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/oracle.py +0 -0
  77. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/plasma_vault.py +0 -0
  78. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/rewards_manager.py +0 -0
  79. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/simulation.py +0 -0
  80. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  81. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/__init__.py +0 -0
  82. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  83. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  84. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  85. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  86. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  87. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  88. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  89. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  90. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  91. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/contracts.py +0 -0
  92. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/discovery.py +0 -0
  93. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/errors.py +0 -0
  94. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  95. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  96. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  97. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  98. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/lane.py +0 -0
  99. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/logs.py +0 -0
  100. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/messages.py +0 -0
  101. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/simulation.py +0 -0
  102. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  103. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  104. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  105. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  106. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  107. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/crosschain/transport.py +0 -0
  108. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/dependency_graph.py +0 -0
  109. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/entry_cli.py +0 -0
  110. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/entry_mcp.py +0 -0
  111. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/errors.py +0 -0
  112. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/explorer.py +0 -0
  113. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/field_docs.py +0 -0
  114. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/__init__.py +0 -0
  115. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  116. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/async_action.py +0 -0
  117. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/base.py +0 -0
  118. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  119. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/erc4626.py +0 -0
  120. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  121. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/events.py +0 -0
  122. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/external_state.py +0 -0
  123. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  124. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  125. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  126. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/hypercore.py +0 -0
  127. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  128. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/merkl.py +0 -0
  129. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/morpho.py +0 -0
  130. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  131. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  132. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/fuses/universal.py +0 -0
  133. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/guide/__init__.py +0 -0
  134. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/guide/architecture.md +0 -0
  135. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/guide/glossary.md +0 -0
  136. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/guide/invariants.md +0 -0
  137. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/guide/quickstart.md +0 -0
  138. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/market_ids.py +0 -0
  139. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/market_info.py +0 -0
  140. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/mcp/__init__.py +0 -0
  141. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/mcp/guide.py +0 -0
  142. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/mcp/models.py +0 -0
  143. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/mcp/server.py +0 -0
  144. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/morpho_api.py +0 -0
  145. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/py.typed +0 -0
  146. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/__init__.py +0 -0
  147. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/aave_v3.py +0 -0
  148. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/aave_v4.py +0 -0
  149. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/compound_v3.py +0 -0
  150. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/hypercore.py +0 -0
  151. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  152. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/lending_health.py +0 -0
  153. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/morpho.py +0 -0
  154. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  155. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  156. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  157. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/position_manager.py +0 -0
  158. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  159. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  160. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/readers/unpriceable.py +0 -0
  161. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/substrates.py +0 -0
  162. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/types.py +0 -0
  163. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/validation.py +0 -0
  164. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_data.py +0 -0
  165. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_info.py +0 -0
  166. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_managers.py +0 -0
  167. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_positions.py +0 -0
  168. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/src/ipor_fusion/vault_reads.py +0 -0
  169. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/UniswapV3UniversalRouter.py +0 -0
  170. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_crosschain.py +0 -0
  171. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_crosschain_lifecycle.py +0 -0
  172. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_crosschain_recovery.py +0 -0
  173. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_euler_v2.py +0 -0
  174. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_foundry.py +0 -0
  175. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_guide.py +0 -0
  176. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_hypercore_model.py +0 -0
  177. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_hypercore_shadow.py +0 -0
  178. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_multicall.py +0 -0
  179. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_simulate.py +0 -0
  180. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/_validation.py +0 -0
  181. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/addresses.py +0 -0
  182. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/constants.py +0 -0
  183. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  184. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/hypercore_market55.json +0 -0
  185. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/vault_info_golden/clean.json +0 -0
  186. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  187. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/vault_info_golden/rich.json +0 -0
  188. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  189. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_about.py +0 -0
  190. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_access_manager.py +0 -0
  191. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cache.py +0 -0
  192. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_chains.py +0 -0
  193. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_changelog.py +0 -0
  194. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_commands.py +0 -0
  195. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_config_store.py +0 -0
  196. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_hypercore.py +0 -0
  197. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_lending_health.py +0 -0
  198. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_market_morpho_blue.py +0 -0
  199. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_oracle_mapping.py +0 -0
  200. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_pending_requests_scan.py +0 -0
  201. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_role_accounts.py +0 -0
  202. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_vault_info_golden.py +0 -0
  203. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_vault_predates_getters.py +0 -0
  204. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_cli_vault_validate.py +0 -0
  205. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_contract_call.py +0 -0
  206. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_contract_encoder.py +0 -0
  207. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_ccip_events.py +0 -0
  208. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_ccip_indexer.py +0 -0
  209. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_ccip_quote.py +0 -0
  210. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_chainlink.py +0 -0
  211. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_contracts.py +0 -0
  212. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_encoding.py +0 -0
  213. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_errors.py +0 -0
  214. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_lane.py +0 -0
  215. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_layering.py +0 -0
  216. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_readiness.py +0 -0
  217. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_readiness_report.py +0 -0
  218. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_crosschain_transport.py +0 -0
  219. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_erc20.py +0 -0
  220. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_error_hierarchy.py +0 -0
  221. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_examples_composed_hypercore.py +0 -0
  222. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_examples_crosschain.py +0 -0
  223. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  224. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_examples_vaults.py +0 -0
  225. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_explorer.py +0 -0
  226. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_external_state_events.py +0 -0
  227. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_external_state_executor.py +0 -0
  228. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_fee_manager.py +0 -0
  229. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_fuse_encoding.py +0 -0
  230. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_fusion_instance.py +0 -0
  231. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_guide.py +0 -0
  232. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_deployment.py +0 -0
  233. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_events.py +0 -0
  234. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_flow.py +0 -0
  235. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_live.py +0 -0
  236. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_outcome.py +0 -0
  237. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_readers.py +0 -0
  238. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_hypercore_simulation.py +0 -0
  239. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_logs.py +0 -0
  240. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_mcp_hypercore.py +0 -0
  241. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_mcp_models.py +0 -0
  242. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_mcp_server.py +0 -0
  243. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_mcp_vault_validate.py +0 -0
  244. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_multicall.py +0 -0
  245. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_oracle.py +0 -0
  246. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_oracle_mapping.py +0 -0
  247. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_plasma_vault.py +0 -0
  248. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_public_api.py +0 -0
  249. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_reader_encoding.py +0 -0
  250. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_readiness_selection.py +0 -0
  251. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_revert_decoding.py +0 -0
  252. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_rewards_manager.py +0 -0
  253. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_roles.py +0 -0
  254. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_sdk_layering.py +0 -0
  255. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  256. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  257. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  258. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_crosschain_canary.py +0 -0
  259. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_crosschain_hypercore.py +0 -0
  260. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  261. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  262. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  263. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_examples_arbitrum.py +0 -0
  268. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_examples_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_examples_crosschain.py +0 -0
  270. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_external_state_executor_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_morpho_blue.py +0 -0
  275. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_pilot_v3.py +0 -0
  276. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  277. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  278. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  279. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  280. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_readers_integration.py +0 -0
  281. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_release_funds.py +0 -0
  282. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_swap_on_base.py +0 -0
  283. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  284. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_solidity_compiler.py +0 -0
  285. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_solidity_mirrors.py +0 -0
  286. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_substrates.py +0 -0
  287. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_types_and_events.py +0 -0
  288. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_unpriceable_tokens.py +0 -0
  289. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_validation.py +0 -0
  290. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_vault_dep_graph.py +0 -0
  291. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_vault_info_onchain_base.py +0 -0
  292. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  293. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_vault_simulator_blocks.py +0 -0
  294. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/test_withdraw_manager.py +0 -0
  295. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/typed_client/consumer.py +0 -0
  296. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/tests/typed_client/rejected.py +0 -0
  297. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev116}/uv.lock +0 -0
@@ -394,7 +394,9 @@ byte, e.g. Ebisu ZAPPER/REGISTRY).
394
394
  role-accounts, oracle-mapping). `vault info` runs independent fetch pipelines
395
395
  (vault/asset, fees, WithdrawManager, markets, roles) concurrently on a
396
396
  `ThreadPoolExecutor`, each a short chain of `Multicall3` batches; keep new
397
- reads inside a batch rather than adding per-call round trips. Contract names,
397
+ reads inside a batch rather than adding per-call round trips. A `from_url` context
398
+ bounds requests in flight (`max_concurrent_requests`, default 8) whatever the pool
399
+ nesting, so a new fan-out does not need its own limit. Contract names,
398
400
  token symbols and decimals and deployment blocks are cached per chain in
399
401
  `$XDG_CACHE_HOME/ipor-fusion/contract_cache.json` (`<chain>:<address>:<field>`
400
402
  keys); tests redirect it with `monkeypatch.setenv("XDG_CACHE_HOME", ...)` or
@@ -98,6 +98,12 @@ converted amounts `Amount`. A custom `VaultState` annotates the same.
98
98
  now shown in share decimals labelled `shares` (``fusion vault info``) and its JSON
99
99
  ``formatted`` uses `share_decimals`; it was formatted with the asset's decimals.
100
100
 
101
+ `Web3Context.from_url(..., max_concurrent_requests=8)` bounds the RPC requests in
102
+ flight across every thread using the context (``None`` lifts the bound); the vault
103
+ reads fan out on nested thread pools, and the bound is what a rate-limited provider
104
+ sees. A context built from your own `Web3` object is not bounded unless you add the
105
+ same middleware.
106
+
101
107
  `ensure_supported_chain` takes `ChainId`. `fetch_unpriceable_priced_tokens` takes the oracle and
102
108
  underlying as `ChecksumAddress`. `resolve_token_symbol`, `resolve_token_decimals`,
103
109
  `get_contract_name` and `get_deployment_tx` take `HexAddress` (a checksum address
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev114
3
+ Version: 4.0.0.dev116
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ipor-fusion"
3
- version = "4.0.0.dev114"
3
+ version = "4.0.0.dev116"
4
4
  description = "The IPOR Fusion SDK for Python"
5
5
  authors = [
6
6
  { name = "Adam Mydlarz", email = "amydlarz@gmail.com" },
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import copy
4
4
  import logging
5
+ import threading
6
+ from typing import Any
5
7
 
6
8
  from eth_account import Account
7
9
  from eth_typing import ChecksumAddress, HexStr
@@ -11,12 +13,21 @@ from requests import ConnectionError as RequestsConnectionError
11
13
  from requests import HTTPError, Timeout
12
14
  from web3 import Web3
13
15
  from web3.exceptions import ContractLogicError, Web3RPCError
14
- from web3.middleware import ExtraDataToPOAMiddleware
16
+ from web3.middleware import ExtraDataToPOAMiddleware, Web3Middleware
15
17
  from web3.providers.rpc.utils import (
16
18
  REQUEST_RETRY_ALLOWLIST,
17
19
  ExceptionRetryConfiguration,
18
20
  )
19
- from web3.types import BlockIdentifier, LogReceipt, TxParams, TxReceipt, Wei
21
+ from web3.types import (
22
+ BlockIdentifier,
23
+ LogReceipt,
24
+ MakeRequestFn,
25
+ RPCEndpoint,
26
+ RPCResponse,
27
+ TxParams,
28
+ TxReceipt,
29
+ Wei,
30
+ )
20
31
 
21
32
  from ipor_fusion.cache import ContractCache, default_contract_cache
22
33
  from ipor_fusion.chains import GET_LOGS_RANGE_HINTS
@@ -70,6 +81,11 @@ class Web3Context:
70
81
  # multi-call read (vault fetch, health check) fans that out into minutes of
71
82
  # wall clock. Long-running services should pass something tighter.
72
83
  DEFAULT_RPC_TIMEOUT_S = 30.0
84
+ # RPC requests in flight at once through a `from_url` context. The vault
85
+ # reads fan out on nested thread pools; this bounds what reaches the
86
+ # provider, whatever the nesting, so a rate-limited endpoint sees a steady
87
+ # stream instead of bursts.
88
+ DEFAULT_MAX_CONCURRENT_REQUESTS = 8
73
89
 
74
90
  def __init__(
75
91
  self,
@@ -142,7 +158,7 @@ class Web3Context:
142
158
  return self._signer
143
159
 
144
160
  @classmethod
145
- def from_url(
161
+ def from_url( # proctor: ignore[primitives-too-many-params]
146
162
  cls,
147
163
  url: str,
148
164
  *,
@@ -153,7 +169,11 @@ class Web3Context:
153
169
  log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
154
170
  log_source: LogSource | None = None,
155
171
  cache: ContractCache | None = None,
172
+ max_concurrent_requests: int | None = DEFAULT_MAX_CONCURRENT_REQUESTS,
156
173
  ) -> Web3Context:
174
+ """A context on an HTTP provider at ``url``. ``max_concurrent_requests``
175
+ bounds the RPC requests in flight across every thread using it
176
+ (``None``: unbounded)."""
157
177
  web3 = Web3(
158
178
  Web3.HTTPProvider(
159
179
  url,
@@ -171,6 +191,8 @@ class Web3Context:
171
191
  # with a ValueError. The middleware moves it to proofOfAuthorityData
172
192
  # and leaves every other block untouched.
173
193
  web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
194
+ if max_concurrent_requests is not None:
195
+ web3.middleware_onion.add(_in_flight_limit(max_concurrent_requests))
174
196
  chain_id = ChainId(web3.eth.chain_id)
175
197
 
176
198
  return cls(
@@ -385,3 +407,21 @@ class Web3Context:
385
407
  @staticmethod
386
408
  def _percent_of(value: int, percentage: int) -> int:
387
409
  return value * percentage // 100
410
+
411
+
412
+ def _in_flight_limit(limit: int) -> type[Web3Middleware]:
413
+ """A middleware class (web3 instantiates it per Web3) holding a semaphore
414
+ slot for the duration of each provider request."""
415
+ if limit < 1:
416
+ raise ValueError(f"max_concurrent_requests must be at least 1, got {limit}")
417
+ slots = threading.BoundedSemaphore(limit)
418
+
419
+ class InFlightLimit(Web3Middleware):
420
+ def wrap_make_request(self, make_request: MakeRequestFn) -> MakeRequestFn:
421
+ def limited(method: RPCEndpoint, params: Any) -> RPCResponse:
422
+ with slots:
423
+ return make_request(method, params)
424
+
425
+ return limited
426
+
427
+ return InFlightLimit
@@ -5,21 +5,21 @@ middleware price per token, valued in the underlying asset. Part of the
5
5
  from __future__ import annotations
6
6
 
7
7
  from collections.abc import Callable, Mapping, Sequence
8
- from concurrent.futures import Future, ThreadPoolExecutor
9
8
  from dataclasses import dataclass
10
- from functools import partial
9
+ from typing import Any
11
10
 
12
11
  from eth_typing import ChecksumAddress, HexAddress
13
12
  from web3 import Web3
14
13
 
15
14
  from ipor_fusion.core.context import Web3Context
15
+ from ipor_fusion.core.contract import Call
16
16
  from ipor_fusion.core.erc20 import ERC20
17
+ from ipor_fusion.core.multicall import Multicall3
17
18
  from ipor_fusion.core.oracle import PriceOracleMiddleware
18
19
  from ipor_fusion.core.plasma_vault import BalanceFuse
19
20
  from ipor_fusion.market_ids import IporFusionMarkets
20
21
  from ipor_fusion.substrates import decode_substrate
21
22
  from ipor_fusion.types import MarketId, PlasmaVaultAddress
22
- from ipor_fusion.vault_reads import _safe_call
23
23
 
24
24
 
25
25
  @dataclass(frozen=True)
@@ -113,36 +113,32 @@ def _read_tokens(
113
113
  addresses: list[HexAddress],
114
114
  resolve_symbol: Callable[[HexAddress], str],
115
115
  ) -> list[_TokenRead]:
116
- with ThreadPoolExecutor() as pool:
117
- futures: list[tuple[HexAddress, Future, Future, Future, Future]] = []
118
- for address in addresses:
119
- checksum = Web3.to_checksum_address(address)
120
- token = ERC20(checksum)
121
- futures.append(
122
- (
123
- address,
124
- pool.submit(resolve_symbol, address),
125
- pool.submit(_safe_call, partial(token.decimals().call, ctx)),
126
- pool.submit(_safe_call, partial(token.balance_of(vault).call, ctx)),
127
- pool.submit(
128
- _safe_call,
129
- partial(oracle.get_asset_price(checksum).call, ctx),
130
- ),
131
- )
116
+ """Every token's decimals, the vault's balance and the middleware price in
117
+ one Multicall3 batch (a read that fails is ``None``); symbols through
118
+ ``resolve_symbol``."""
119
+ calls: list[Call[Any]] = []
120
+ for address in addresses:
121
+ checksum = Web3.to_checksum_address(address)
122
+ token = ERC20(checksum)
123
+ calls += [
124
+ token.decimals(),
125
+ token.balance_of(vault),
126
+ oracle.get_asset_price(checksum),
127
+ ]
128
+ results = Multicall3(ctx).try_aggregate(calls)
129
+ reads: list[_TokenRead] = []
130
+ for index, address in enumerate(addresses):
131
+ decimals, balance, price = results[3 * index : 3 * index + 3]
132
+ reads.append(
133
+ _TokenRead(
134
+ address=address,
135
+ symbol=resolve_symbol(address) or "?",
136
+ decimals=decimals,
137
+ balance=balance,
138
+ price_usd=price.readable() if price else None,
132
139
  )
133
- reads: list[_TokenRead] = []
134
- for address, symbol, decimals, balance, price in futures:
135
- price_value = price.result()
136
- reads.append(
137
- _TokenRead(
138
- address=address,
139
- symbol=symbol.result() or "?",
140
- decimals=decimals.result(),
141
- balance=balance.result(),
142
- price_usd=price_value.readable() if price_value else None,
143
- )
144
- )
145
- return reads
140
+ )
141
+ return reads
146
142
 
147
143
 
148
144
  def _value_tokens(
@@ -49,6 +49,7 @@ def sequential_multicall():
49
49
  patch("ipor_fusion.vault_reads.Multicall3", SequentialMulticall),
50
50
  patch("ipor_fusion.vault_positions.Multicall3", SequentialMulticall),
51
51
  patch("ipor_fusion.vault_data.Multicall3", SequentialMulticall),
52
+ patch("ipor_fusion.vault_erc20.Multicall3", SequentialMulticall),
52
53
  ):
53
54
  yield
54
55
 
@@ -752,6 +752,7 @@ class FakeBalanceFuse:
752
752
  fuse: str
753
753
 
754
754
 
755
+ @pytest.mark.usefixtures("sequential_multicall")
755
756
  class TestPrintErc20Balances:
756
757
  @pytest.fixture(autouse=True)
757
758
  def _tmp_config(self, tmp_path, monkeypatch):
@@ -1227,6 +1228,7 @@ class TestPrintReconciliation:
1227
1228
  assert "MISMATCH" in captured.out
1228
1229
 
1229
1230
 
1231
+ @pytest.mark.usefixtures("sequential_multicall")
1230
1232
  class TestErc20BalancesNotes:
1231
1233
  @pytest.fixture(autouse=True)
1232
1234
  def _tmp_config(self, tmp_path, monkeypatch):
@@ -1,6 +1,9 @@
1
1
  # pyright: reportAttributeAccessIssue=false
2
2
  """Unit tests for Web3Context — mock Web3 calls, no network required."""
3
3
 
4
+ import threading
5
+ import time
6
+ from concurrent.futures import ThreadPoolExecutor
4
7
  from unittest.mock import MagicMock, patch
5
8
 
6
9
  import pytest
@@ -126,7 +129,7 @@ class TestFromUrl:
126
129
 
127
130
  def make_request(_provider, method, _params):
128
131
  methods.append(method)
129
- result = "0x1" if method == "eth_chainId" else "0x" + "00" * 32
132
+ result = "0x1" + "00" * 32
130
133
  return {"jsonrpc": "2.0", "id": 1, "result": result}
131
134
 
132
135
  with patch.object(HTTPProvider, "make_request", make_request):
@@ -635,3 +638,42 @@ class TestPinnedContext:
635
638
  assert ctx.call.call_args.args == (ADDR, b"\x01")
636
639
  call.call(ctx, block=55)
637
640
  assert ctx.call.call_args.args == (ADDR, b"\x01", 55)
641
+
642
+
643
+ class TestConcurrencyLimit:
644
+ def _peak(self, limit: int | None) -> int:
645
+ in_flight = 0
646
+ peak = 0
647
+ lock = threading.Lock()
648
+
649
+ def make_request(provider, method, params):
650
+ nonlocal in_flight, peak
651
+ with lock:
652
+ in_flight += 1
653
+ peak = max(peak, in_flight)
654
+ time.sleep(0.02)
655
+ with lock:
656
+ in_flight -= 1
657
+ result = "0x1"
658
+ return {"jsonrpc": "2.0", "id": 1, "result": result}
659
+
660
+ with patch.object(HTTPProvider, "make_request", make_request):
661
+ ctx = Web3Context.from_url(
662
+ "http://localhost:8545", max_concurrent_requests=limit
663
+ )
664
+ with ThreadPoolExecutor(16) as pool:
665
+ list(pool.map(lambda _: ctx.web3.eth.block_number, range(32)))
666
+ return peak
667
+
668
+ def test_requests_in_flight_never_exceed_the_limit(self):
669
+ assert self._peak(3) <= 3
670
+
671
+ def test_none_leaves_requests_unbounded(self):
672
+ assert self._peak(None) > 3
673
+
674
+ def test_a_limit_below_one_is_rejected(self):
675
+ with (
676
+ patch.object(HTTPProvider, "make_request"),
677
+ pytest.raises(ValueError, match="at least 1"),
678
+ ):
679
+ Web3Context.from_url("http://localhost:8545", max_concurrent_requests=0)