ipor-fusion 4.0.0.dev114__tar.gz → 4.0.0.dev118__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.dev118}/AGENTS.md +3 -1
  2. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/MIGRATION_4.md +6 -0
  3. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/PKG-INFO +1 -1
  4. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/pyproject.toml +1 -1
  5. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/context.py +63 -3
  6. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_erc20.py +28 -32
  7. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/conftest.py +1 -0
  8. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_helpers.py +2 -0
  9. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_web3_context.py +141 -1
  10. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.claude-plugin/marketplace.json +0 -0
  11. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.claude-plugin/plugin.json +0 -0
  12. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.env.example +0 -0
  13. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.gemini/settings.json +0 -0
  14. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.git-blame-ignore-revs +0 -0
  15. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/cd.yml +0 -0
  16. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/ci.yml +0 -0
  17. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/commit-lint.yml +0 -0
  18. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/notify-slack.yml +0 -0
  19. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/python-build.yml +0 -0
  20. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/python-publish.yml +0 -0
  21. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/release.yml +0 -0
  22. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.github/workflows/report-slack.yml +0 -0
  23. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.gitignore +0 -0
  24. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.mcp.json +0 -0
  25. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.pre-commit-config.yaml +0 -0
  26. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/.python-version +0 -0
  27. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/CHANGELOG.md +0 -0
  28. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/CLAUDE.md +0 -0
  29. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/CODEOWNERS +0 -0
  30. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/LICENSE +0 -0
  31. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/README.md +0 -0
  32. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/build.sh +0 -0
  33. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/CONTRIBUTING.md +0 -0
  34. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/README.md +0 -0
  35. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/advanced_euler_v2_credit_market_base.py +0 -0
  36. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/advanced_euler_v2_looping_arbitrum.py +0 -0
  37. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +0 -0
  38. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +0 -0
  39. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/external_state_margin_leg_base.py +0 -0
  40. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/examples/simple_aave_v3_supply_base.py +0 -0
  41. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/pytest.ini +0 -0
  42. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/scripts/commit-check.sh +0 -0
  43. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/scripts/crosschain_readiness.py +0 -0
  44. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/skills/ipor-deploy-vault/SKILL.md +0 -0
  45. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/__init__.py +0 -0
  46. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/about.py +0 -0
  47. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cache.py +0 -0
  48. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/chains.py +0 -0
  49. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/__init__.py +0 -0
  50. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  51. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/config_cmd.py +0 -0
  52. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/config_store.py +0 -0
  53. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/main.py +0 -0
  54. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/market_cmd.py +0 -0
  55. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/options.py +0 -0
  56. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  57. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  58. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/vault_health.py +0 -0
  59. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  60. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  61. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  62. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/config/__init__.py +0 -0
  63. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/config/roles.py +0 -0
  64. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/__init__.py +0 -0
  65. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/access.py +0 -0
  66. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/address.py +0 -0
  67. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/contract.py +0 -0
  68. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/erc20.py +0 -0
  69. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/external_state_executor.py +0 -0
  70. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/fee_manager.py +0 -0
  71. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/fuse.py +0 -0
  72. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/fuse_action.py +0 -0
  73. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/fusion_factory.py +0 -0
  74. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/logs.py +0 -0
  75. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/multicall.py +0 -0
  76. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/oracle.py +0 -0
  77. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/plasma_vault.py +0 -0
  78. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/rewards_manager.py +0 -0
  79. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/simulation.py +0 -0
  80. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  81. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/__init__.py +0 -0
  82. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  83. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  84. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  85. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  86. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  87. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  88. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  89. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  90. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  91. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/contracts.py +0 -0
  92. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/discovery.py +0 -0
  93. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/errors.py +0 -0
  94. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  95. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  96. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  97. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  98. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/lane.py +0 -0
  99. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/logs.py +0 -0
  100. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/messages.py +0 -0
  101. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/simulation.py +0 -0
  102. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  103. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  104. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/stargate/lane.py +0 -0
  105. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  106. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  107. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/crosschain/transport.py +0 -0
  108. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/dependency_graph.py +0 -0
  109. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/entry_cli.py +0 -0
  110. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/entry_mcp.py +0 -0
  111. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/errors.py +0 -0
  112. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/explorer.py +0 -0
  113. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/field_docs.py +0 -0
  114. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/__init__.py +0 -0
  115. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  116. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/async_action.py +0 -0
  117. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/base.py +0 -0
  118. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  119. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/erc4626.py +0 -0
  120. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  121. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/events.py +0 -0
  122. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/external_state.py +0 -0
  123. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  124. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  125. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  126. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/hypercore.py +0 -0
  127. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  128. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/merkl.py +0 -0
  129. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/morpho.py +0 -0
  130. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  131. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  132. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/fuses/universal.py +0 -0
  133. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/guide/__init__.py +0 -0
  134. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/guide/architecture.md +0 -0
  135. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/guide/glossary.md +0 -0
  136. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/guide/invariants.md +0 -0
  137. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/guide/quickstart.md +0 -0
  138. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/market_ids.py +0 -0
  139. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/market_info.py +0 -0
  140. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/mcp/__init__.py +0 -0
  141. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/mcp/guide.py +0 -0
  142. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/mcp/models.py +0 -0
  143. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/mcp/server.py +0 -0
  144. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/morpho_api.py +0 -0
  145. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/py.typed +0 -0
  146. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/__init__.py +0 -0
  147. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/aave_v3.py +0 -0
  148. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/aave_v4.py +0 -0
  149. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/compound_v3.py +0 -0
  150. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/hypercore.py +0 -0
  151. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/hypercore_outcome.py +0 -0
  152. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/lending_health.py +0 -0
  153. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/morpho.py +0 -0
  154. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  155. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  156. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/oracle_mapping_reader.py +0 -0
  157. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/position_manager.py +0 -0
  158. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  159. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  160. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/readers/unpriceable.py +0 -0
  161. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/substrates.py +0 -0
  162. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/types.py +0 -0
  163. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/validation.py +0 -0
  164. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_data.py +0 -0
  165. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_info.py +0 -0
  166. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_managers.py +0 -0
  167. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_positions.py +0 -0
  168. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/src/ipor_fusion/vault_reads.py +0 -0
  169. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/UniswapV3UniversalRouter.py +0 -0
  170. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_crosschain.py +0 -0
  171. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_crosschain_lifecycle.py +0 -0
  172. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_crosschain_recovery.py +0 -0
  173. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_euler_v2.py +0 -0
  174. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_foundry.py +0 -0
  175. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_guide.py +0 -0
  176. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_hypercore_model.py +0 -0
  177. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_hypercore_shadow.py +0 -0
  178. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_multicall.py +0 -0
  179. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_simulate.py +0 -0
  180. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/_validation.py +0 -0
  181. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/addresses.py +0 -0
  182. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/constants.py +0 -0
  183. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  184. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/hypercore_market55.json +0 -0
  185. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/vault_info_golden/clean.json +0 -0
  186. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  187. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/vault_info_golden/rich.json +0 -0
  188. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  189. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_about.py +0 -0
  190. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_access_manager.py +0 -0
  191. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cache.py +0 -0
  192. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_chains.py +0 -0
  193. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_changelog.py +0 -0
  194. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_commands.py +0 -0
  195. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_config_store.py +0 -0
  196. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_hypercore.py +0 -0
  197. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_lending_health.py +0 -0
  198. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_market_morpho_blue.py +0 -0
  199. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_oracle_mapping.py +0 -0
  200. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_pending_requests_scan.py +0 -0
  201. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_role_accounts.py +0 -0
  202. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_vault_info_golden.py +0 -0
  203. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_vault_predates_getters.py +0 -0
  204. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_cli_vault_validate.py +0 -0
  205. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_contract_call.py +0 -0
  206. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_contract_encoder.py +0 -0
  207. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_ccip_events.py +0 -0
  208. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_ccip_indexer.py +0 -0
  209. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_ccip_quote.py +0 -0
  210. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_chainlink.py +0 -0
  211. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_contracts.py +0 -0
  212. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_encoding.py +0 -0
  213. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_errors.py +0 -0
  214. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_lane.py +0 -0
  215. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_layering.py +0 -0
  216. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_readiness.py +0 -0
  217. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_readiness_report.py +0 -0
  218. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_crosschain_transport.py +0 -0
  219. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_erc20.py +0 -0
  220. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_error_hierarchy.py +0 -0
  221. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_examples_composed_hypercore.py +0 -0
  222. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_examples_crosschain.py +0 -0
  223. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  224. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_examples_vaults.py +0 -0
  225. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_explorer.py +0 -0
  226. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_external_state_events.py +0 -0
  227. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_external_state_executor.py +0 -0
  228. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_fee_manager.py +0 -0
  229. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_fuse_encoding.py +0 -0
  230. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_fusion_instance.py +0 -0
  231. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_guide.py +0 -0
  232. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_deployment.py +0 -0
  233. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_events.py +0 -0
  234. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_flow.py +0 -0
  235. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_live.py +0 -0
  236. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_outcome.py +0 -0
  237. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_readers.py +0 -0
  238. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_hypercore_simulation.py +0 -0
  239. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_logs.py +0 -0
  240. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_mcp_hypercore.py +0 -0
  241. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_mcp_models.py +0 -0
  242. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_mcp_server.py +0 -0
  243. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_mcp_vault_validate.py +0 -0
  244. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_multicall.py +0 -0
  245. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_oracle.py +0 -0
  246. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_oracle_mapping.py +0 -0
  247. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_plasma_vault.py +0 -0
  248. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_public_api.py +0 -0
  249. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_reader_encoding.py +0 -0
  250. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_readiness_selection.py +0 -0
  251. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_revert_decoding.py +0 -0
  252. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_rewards_manager.py +0 -0
  253. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_roles.py +0 -0
  254. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_sdk_layering.py +0 -0
  255. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_borrow_aave_v3_mainnet.py +0 -0
  256. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_claim_merkl_wrapper_base.py +0 -0
  257. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_claim_morpho_rewards.py +0 -0
  258. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_crosschain_canary.py +0 -0
  259. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_crosschain_hypercore.py +0 -0
  260. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_crosschain_hyperevm.py +0 -0
  261. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  262. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_euler_v2_batch_base.py +0 -0
  263. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_euler_v2_credit_market_base.py +0 -0
  264. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_euler_v2_swap_deploy_base.py +0 -0
  265. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +0 -0
  266. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_euler_v2_swap_registry_base.py +0 -0
  267. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_examples_arbitrum.py +0 -0
  268. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_examples_base.py +0 -0
  269. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_examples_crosschain.py +0 -0
  270. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_external_state_executor_base.py +0 -0
  271. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  272. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_looping_aave_v3_base.py +0 -0
  273. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_looping_morpho_blue_base.py +0 -0
  274. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_morpho_blue.py +0 -0
  275. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_pilot_v3.py +0 -0
  276. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_pilot_v4_uniswap.py +0 -0
  277. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_pilot_v5_ramses.py +0 -0
  278. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_pilot_v5_scheduled.py +0 -0
  279. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  280. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_readers_integration.py +0 -0
  281. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_release_funds.py +0 -0
  282. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_swap_on_base.py +0 -0
  283. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_simulate_vault_from_scratch_base.py +0 -0
  284. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_solidity_compiler.py +0 -0
  285. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_solidity_mirrors.py +0 -0
  286. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_substrates.py +0 -0
  287. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_types_and_events.py +0 -0
  288. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_unpriceable_tokens.py +0 -0
  289. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_validation.py +0 -0
  290. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_vault_dep_graph.py +0 -0
  291. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_vault_info_onchain_base.py +0 -0
  292. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  293. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_vault_simulator_blocks.py +0 -0
  294. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/test_withdraw_manager.py +0 -0
  295. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/typed_client/consumer.py +0 -0
  296. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/tests/typed_client/rejected.py +0 -0
  297. {ipor_fusion-4.0.0.dev114 → ipor_fusion-4.0.0.dev118}/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.dev118
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.dev118"
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,22 @@ 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
+ MakeBatchRequestFn,
25
+ MakeRequestFn,
26
+ RPCEndpoint,
27
+ RPCResponse,
28
+ TxParams,
29
+ TxReceipt,
30
+ Wei,
31
+ )
20
32
 
21
33
  from ipor_fusion.cache import ContractCache, default_contract_cache
22
34
  from ipor_fusion.chains import GET_LOGS_RANGE_HINTS
@@ -70,6 +82,11 @@ class Web3Context:
70
82
  # multi-call read (vault fetch, health check) fans that out into minutes of
71
83
  # wall clock. Long-running services should pass something tighter.
72
84
  DEFAULT_RPC_TIMEOUT_S = 30.0
85
+ # RPC requests in flight at once through a `from_url` context. The vault
86
+ # reads fan out on nested thread pools; this bounds what reaches the
87
+ # provider, whatever the nesting, so a rate-limited endpoint sees a steady
88
+ # stream instead of bursts.
89
+ DEFAULT_MAX_CONCURRENT_REQUESTS = 8
73
90
 
74
91
  def __init__(
75
92
  self,
@@ -142,7 +159,7 @@ class Web3Context:
142
159
  return self._signer
143
160
 
144
161
  @classmethod
145
- def from_url(
162
+ def from_url( # proctor: ignore[primitives-too-many-params]
146
163
  cls,
147
164
  url: str,
148
165
  *,
@@ -153,7 +170,11 @@ class Web3Context:
153
170
  log_scan_max_requests: int | None = DEFAULT_LOG_SCAN_MAX_REQUESTS,
154
171
  log_source: LogSource | None = None,
155
172
  cache: ContractCache | None = None,
173
+ max_concurrent_requests: int | None = DEFAULT_MAX_CONCURRENT_REQUESTS,
156
174
  ) -> Web3Context:
175
+ """A context on an HTTP provider at ``url``. ``max_concurrent_requests``
176
+ bounds the RPC requests in flight across every thread using it
177
+ (``None``: unbounded)."""
157
178
  web3 = Web3(
158
179
  Web3.HTTPProvider(
159
180
  url,
@@ -171,6 +192,15 @@ class Web3Context:
171
192
  # with a ValueError. The middleware moves it to proofOfAuthorityData
172
193
  # and leaves every other block untouched.
173
194
  web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
195
+ if max_concurrent_requests is not None:
196
+ # Innermost (layer 0, injected last): a slot covers only the provider
197
+ # call, so a middleware that issues its own request (the gas price
198
+ # strategy reads a block) never waits on the slot it already holds.
199
+ web3.middleware_onion.inject(
200
+ _in_flight_limit(max_concurrent_requests),
201
+ name="in_flight_limit",
202
+ layer=0,
203
+ )
174
204
  chain_id = ChainId(web3.eth.chain_id)
175
205
 
176
206
  return cls(
@@ -385,3 +415,33 @@ class Web3Context:
385
415
  @staticmethod
386
416
  def _percent_of(value: int, percentage: int) -> int:
387
417
  return value * percentage // 100
418
+
419
+
420
+ def _in_flight_limit(limit: int) -> type[Web3Middleware]:
421
+ """A middleware class (web3 instantiates it per Web3) holding a semaphore
422
+ slot for the duration of each provider call; a JSON-RPC batch is one HTTP
423
+ request, so it takes one slot whatever its item count."""
424
+ if limit < 1:
425
+ raise ValueError(f"max_concurrent_requests must be at least 1, got {limit}")
426
+ slots = threading.BoundedSemaphore(limit)
427
+
428
+ class InFlightLimit(Web3Middleware):
429
+ def wrap_make_request(self, make_request: MakeRequestFn) -> MakeRequestFn:
430
+ def limited(method: RPCEndpoint, params: Any) -> RPCResponse:
431
+ with slots:
432
+ return make_request(method, params)
433
+
434
+ return limited
435
+
436
+ def wrap_make_batch_request(
437
+ self, make_batch_request: MakeBatchRequestFn
438
+ ) -> MakeBatchRequestFn:
439
+ def limited(
440
+ requests_info: list[tuple[RPCEndpoint, Any]],
441
+ ) -> list[RPCResponse] | RPCResponse:
442
+ with slots:
443
+ return make_batch_request(requests_info)
444
+
445
+ return limited
446
+
447
+ 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,10 @@
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
7
+ from typing import Any
4
8
  from unittest.mock import MagicMock, patch
5
9
 
6
10
  import pytest
@@ -126,7 +130,7 @@ class TestFromUrl:
126
130
 
127
131
  def make_request(_provider, method, _params):
128
132
  methods.append(method)
129
- result = "0x1" if method == "eth_chainId" else "0x" + "00" * 32
133
+ result = "0x1" + "00" * 32
130
134
  return {"jsonrpc": "2.0", "id": 1, "result": result}
131
135
 
132
136
  with patch.object(HTTPProvider, "make_request", make_request):
@@ -635,3 +639,139 @@ class TestPinnedContext:
635
639
  assert ctx.call.call_args.args == (ADDR, b"\x01")
636
640
  call.call(ctx, block=55)
637
641
  assert ctx.call.call_args.args == (ADDR, b"\x01", 55)
642
+
643
+
644
+ class TestConcurrencyLimit:
645
+ def _peak(self, limit: int | None) -> int:
646
+ in_flight = 0
647
+ peak = 0
648
+ lock = threading.Lock()
649
+
650
+ def make_request(provider, method, params):
651
+ nonlocal in_flight, peak
652
+ with lock:
653
+ in_flight += 1
654
+ peak = max(peak, in_flight)
655
+ time.sleep(0.02)
656
+ with lock:
657
+ in_flight -= 1
658
+ result = "0x1"
659
+ return {"jsonrpc": "2.0", "id": 1, "result": result}
660
+
661
+ with patch.object(HTTPProvider, "make_request", make_request):
662
+ ctx = Web3Context.from_url(
663
+ "http://localhost:8545", max_concurrent_requests=limit
664
+ )
665
+ with ThreadPoolExecutor(16) as pool:
666
+ list(pool.map(lambda _: ctx.web3.eth.block_number, range(32)))
667
+ return peak
668
+
669
+ def test_requests_in_flight_never_exceed_the_limit(self):
670
+ assert self._peak(3) <= 3
671
+
672
+ def test_none_leaves_requests_unbounded(self):
673
+ assert self._peak(None) > 3
674
+
675
+ def test_a_nested_middleware_request_does_not_deadlock_at_limit_one(self):
676
+ """web3's gas price strategy reads a block while handling
677
+ eth_sendTransaction; that nested request must not wait on the slot the
678
+ outer one would hold."""
679
+ block = {
680
+ "number": "0x1",
681
+ "hash": "0x" + "11" * 32,
682
+ "parentHash": "0x" + "22" * 32,
683
+ "timestamp": "0x1",
684
+ "baseFeePerGas": "0x1",
685
+ "transactions": [],
686
+ }
687
+
688
+ def make_request(provider, method, params):
689
+ result: Any = {
690
+ "eth_chainId": "0x1",
691
+ "eth_getBlockByNumber": block,
692
+ "eth_sendTransaction": "0x" + "ab" * 32,
693
+ }.get(method, "0x1")
694
+ return {"jsonrpc": "2.0", "id": 1, "result": result}
695
+
696
+ with patch.object(HTTPProvider, "make_request", make_request):
697
+ ctx = Web3Context.from_url(
698
+ "http://localhost:8545", max_concurrent_requests=1
699
+ )
700
+ done = threading.Event()
701
+
702
+ def send():
703
+ ctx.web3.eth.send_transaction(
704
+ {"from": ADDR, "to": ADDR, "gas": 21_000, "gasPrice": 1}
705
+ )
706
+ done.set()
707
+
708
+ worker = threading.Thread(target=send, daemon=True)
709
+ worker.start()
710
+ worker.join(timeout=2)
711
+ assert done.is_set()
712
+
713
+ def _batch_peak(self, limit: int) -> int:
714
+ in_flight = 0
715
+ peak = 0
716
+ lock = threading.Lock()
717
+
718
+ def make_request(provider, method, params):
719
+ return {"jsonrpc": "2.0", "id": 1, "result": "0x1"}
720
+
721
+ def make_batch_request(provider, requests):
722
+ nonlocal in_flight, peak
723
+ with lock:
724
+ in_flight += 1
725
+ peak = max(peak, in_flight)
726
+ time.sleep(0.02)
727
+ with lock:
728
+ in_flight -= 1
729
+ return [
730
+ {"jsonrpc": "2.0", "id": i, "result": "0x1"}
731
+ for i, _ in enumerate(requests)
732
+ ]
733
+
734
+ with (
735
+ patch.object(HTTPProvider, "make_request", make_request),
736
+ patch.object(HTTPProvider, "make_batch_request", make_batch_request),
737
+ ):
738
+ ctx = Web3Context.from_url(
739
+ "http://localhost:8545", max_concurrent_requests=limit
740
+ )
741
+
742
+ def one_batch(_):
743
+ with ctx.web3.batch_requests() as batch:
744
+ batch.add(ctx.web3.eth.get_block_number())
745
+ return batch.execute()
746
+
747
+ with ThreadPoolExecutor(8) as pool:
748
+ list(pool.map(one_batch, range(16)))
749
+ return peak
750
+
751
+ def test_batches_take_one_slot_each_within_the_limit(self):
752
+ assert self._batch_peak(2) <= 2
753
+
754
+ def test_a_failed_request_releases_its_slot(self):
755
+ calls = 0
756
+
757
+ def make_request(provider, method, params):
758
+ nonlocal calls
759
+ calls += 1
760
+ if calls == 2:
761
+ raise ConnectionError("dropped")
762
+ return {"jsonrpc": "2.0", "id": 1, "result": "0x1"}
763
+
764
+ with patch.object(HTTPProvider, "make_request", make_request):
765
+ ctx = Web3Context.from_url(
766
+ "http://localhost:8545", max_concurrent_requests=1
767
+ )
768
+ with pytest.raises(ConnectionError):
769
+ ctx.web3.eth.block_number # noqa: B018
770
+ assert ctx.web3.eth.block_number == 1
771
+
772
+ def test_a_limit_below_one_is_rejected(self):
773
+ with (
774
+ patch.object(HTTPProvider, "make_request"),
775
+ pytest.raises(ValueError, match="at least 1"),
776
+ ):
777
+ Web3Context.from_url("http://localhost:8545", max_concurrent_requests=0)