ipor-fusion 4.0.0.dev88__tar.gz → 4.0.0.dev103__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 (298) hide show
  1. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/AGENTS.md +50 -21
  2. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/MIGRATION_4.md +120 -36
  3. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/PKG-INFO +16 -13
  4. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/README.md +15 -12
  5. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/CONTRIBUTING.md +1 -1
  6. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/README.md +1 -1
  7. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/advanced_euler_v2_credit_market_base.py +17 -17
  8. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/advanced_euler_v2_looping_arbitrum.py +12 -15
  9. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +23 -25
  10. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +4 -4
  11. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/external_state_margin_leg_base.py +21 -21
  12. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/examples/simple_aave_v3_supply_base.py +13 -13
  13. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/pyproject.toml +4 -2
  14. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/scripts/crosschain_readiness.py +22 -21
  15. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/skills/ipor-deploy-vault/SKILL.md +10 -10
  16. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/__init__.py +0 -2
  17. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/market_cmd.py +3 -3
  18. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_cmd.py +9 -21
  19. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_health.py +1 -15
  20. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_info_view.py +16 -37
  21. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_markets_view.py +44 -96
  22. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/access.py +26 -24
  23. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/context.py +18 -5
  24. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/contract.py +42 -60
  25. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/erc20.py +1 -1
  26. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/external_state_executor.py +30 -53
  27. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/multicall.py +37 -22
  28. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/oracle.py +16 -13
  29. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/plasma_vault.py +35 -30
  30. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/rewards_manager.py +1 -1
  31. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/simulation.py +1 -1
  32. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/withdraw_manager.py +16 -11
  33. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/chainlink.py +10 -12
  34. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/contracts.py +3 -3
  35. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/lane.py +22 -14
  36. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/transport.py +4 -9
  37. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/discovery.py +17 -18
  38. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/lane.py +26 -11
  39. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/contracts.py +2 -2
  40. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/lane.py +7 -6
  41. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/transport.py +4 -9
  42. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/transport.py +2 -7
  43. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/errors.py +1 -1
  44. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/architecture.md +4 -4
  45. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/invariants.md +3 -3
  46. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/quickstart.md +7 -7
  47. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/server.py +10 -23
  48. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/aave_v3.py +10 -11
  49. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/hypercore.py +6 -10
  50. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/hypercore_outcome.py +5 -5
  51. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/lending_health.py +11 -10
  52. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/morpho.py +21 -18
  53. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping.py +4 -4
  54. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping_reader.py +24 -25
  55. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/unpriceable.py +9 -7
  56. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/validation.py +36 -273
  57. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_data.py +217 -89
  58. ipor_fusion-4.0.0.dev103/src/ipor_fusion/vault_erc20.py +208 -0
  59. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_info.py +59 -98
  60. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_managers.py +32 -8
  61. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_positions.py +8 -10
  62. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_reads.py +5 -5
  63. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_crosschain_lifecycle.py +10 -10
  64. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_crosschain_recovery.py +1 -1
  65. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_euler_v2.py +14 -18
  66. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_multicall.py +2 -2
  67. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_validation.py +6 -7
  68. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/conftest.py +1 -0
  69. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_access_manager.py +24 -23
  70. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_commands.py +50 -34
  71. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_helpers.py +165 -127
  72. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_hypercore.py +8 -7
  73. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_lending_health.py +16 -14
  74. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_pending_requests_scan.py +14 -7
  75. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_role_accounts.py +9 -9
  76. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_info_golden.py +51 -16
  77. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_predates_getters.py +9 -8
  78. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_validate.py +5 -2
  79. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_contract_call.py +15 -21
  80. ipor_fusion-4.0.0.dev103/tests/test_contract_encoder.py +158 -0
  81. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_events.py +2 -2
  82. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_indexer.py +1 -1
  83. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_chainlink.py +15 -13
  84. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_contracts.py +28 -27
  85. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_lane.py +125 -20
  86. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_readiness.py +11 -13
  87. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_transport.py +4 -8
  88. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_erc20.py +9 -9
  89. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_examples_vaults.py +2 -2
  90. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_external_state_executor.py +191 -187
  91. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_fee_manager.py +12 -12
  92. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_deployment.py +5 -5
  93. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_flow.py +3 -3
  94. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_live.py +17 -15
  95. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_readers.py +7 -9
  96. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_simulation.py +2 -2
  97. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_server.py +12 -10
  98. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_vault_validate.py +3 -8
  99. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_multicall.py +48 -3
  100. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_oracle.py +65 -23
  101. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_oracle_mapping.py +3 -3
  102. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_plasma_vault.py +62 -60
  103. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_public_api.py +0 -1
  104. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_reader_encoding.py +22 -22
  105. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_rewards_manager.py +12 -12
  106. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_borrow_aave_v3_mainnet.py +11 -15
  107. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_claim_merkl_wrapper_base.py +8 -12
  108. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_claim_morpho_rewards.py +3 -5
  109. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_hypercore.py +4 -6
  110. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_hyperevm.py +63 -62
  111. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_batch_base.py +5 -5
  112. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_credit_market_base.py +4 -8
  113. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_deploy_base.py +6 -6
  114. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +4 -4
  115. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_registry_base.py +5 -5
  116. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_external_state_executor_base.py +17 -7
  117. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_fusion_factory_clone_base.py +12 -16
  118. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_looping_aave_v3_base.py +12 -14
  119. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_looping_morpho_blue_base.py +9 -11
  120. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_morpho_blue.py +5 -5
  121. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v3.py +34 -42
  122. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v4_uniswap.py +26 -38
  123. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v5_ramses.py +18 -26
  124. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v5_scheduled.py +30 -42
  125. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_plasma_vault_setters_base.py +4 -5
  126. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_readers_integration.py +26 -24
  127. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_release_funds.py +11 -15
  128. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_swap_on_base.py +9 -11
  129. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_vault_from_scratch_base.py +5 -5
  130. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_solidity_mirrors.py +1 -1
  131. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_unpriceable_tokens.py +1 -1
  132. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_validation.py +40 -43
  133. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_vault_info_onchain_base.py +3 -4
  134. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +4 -4
  135. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_vault_simulator_blocks.py +1 -1
  136. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_web3_context.py +32 -8
  137. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_withdraw_manager.py +46 -26
  138. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/typed_client/consumer.py +75 -31
  139. ipor_fusion-4.0.0.dev103/tests/typed_client/rejected.py +51 -0
  140. ipor_fusion-4.0.0.dev88/tests/test_contract_encoder.py +0 -118
  141. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.claude-plugin/marketplace.json +0 -0
  142. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.claude-plugin/plugin.json +0 -0
  143. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.env.example +0 -0
  144. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.gemini/settings.json +0 -0
  145. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.git-blame-ignore-revs +0 -0
  146. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/cd.yml +0 -0
  147. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/ci.yml +0 -0
  148. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/commit-lint.yml +0 -0
  149. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/notify-slack.yml +0 -0
  150. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/python-build.yml +0 -0
  151. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/python-publish.yml +0 -0
  152. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/release.yml +0 -0
  153. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.github/workflows/report-slack.yml +0 -0
  154. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.gitignore +0 -0
  155. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.mcp.json +0 -0
  156. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.pre-commit-config.yaml +0 -0
  157. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/.python-version +0 -0
  158. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/CHANGELOG.md +0 -0
  159. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/CLAUDE.md +0 -0
  160. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/CODEOWNERS +0 -0
  161. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/LICENSE +0 -0
  162. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/build.sh +0 -0
  163. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/pytest.ini +0 -0
  164. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/scripts/commit-check.sh +0 -0
  165. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/about.py +0 -0
  166. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cache.py +0 -0
  167. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/chains.py +0 -0
  168. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/__init__.py +0 -0
  169. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  170. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/config_cmd.py +0 -0
  171. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/config_store.py +0 -0
  172. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/main.py +0 -0
  173. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/options.py +0 -0
  174. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  175. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  176. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/config/__init__.py +0 -0
  177. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/config/roles.py +0 -0
  178. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/__init__.py +0 -0
  179. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/address.py +0 -0
  180. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fee_manager.py +0 -0
  181. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fuse.py +0 -0
  182. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fuse_action.py +0 -0
  183. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fusion_factory.py +0 -0
  184. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/logs.py +0 -0
  185. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/__init__.py +0 -0
  186. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  187. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  188. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  189. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  190. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  191. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/contracts.py +0 -0
  192. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/errors.py +0 -0
  193. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/logs.py +0 -0
  194. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/messages.py +0 -0
  195. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/simulation.py +0 -0
  196. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  197. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  198. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/dependency_graph.py +0 -0
  199. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/entry_cli.py +0 -0
  200. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/entry_mcp.py +0 -0
  201. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/explorer.py +0 -0
  202. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/field_docs.py +0 -0
  203. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/__init__.py +0 -0
  204. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  205. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/async_action.py +0 -0
  206. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/base.py +0 -0
  207. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  208. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  209. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  210. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  211. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  212. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/erc4626.py +0 -0
  213. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  214. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/events.py +0 -0
  215. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/external_state.py +0 -0
  216. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  217. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  218. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  219. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/hypercore.py +0 -0
  220. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  221. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/merkl.py +0 -0
  222. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/morpho.py +0 -0
  223. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  224. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  225. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/universal.py +0 -0
  226. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/__init__.py +0 -0
  227. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/glossary.md +0 -0
  228. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/market_ids.py +0 -0
  229. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/market_info.py +0 -0
  230. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/__init__.py +0 -0
  231. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/guide.py +0 -0
  232. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/models.py +0 -0
  233. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/morpho_api.py +0 -0
  234. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/py.typed +0 -0
  235. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/__init__.py +0 -0
  236. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/aave_v4.py +0 -0
  237. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/compound_v3.py +0 -0
  238. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  239. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/position_manager.py +0 -0
  240. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  241. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  242. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/substrates.py +0 -0
  243. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/types.py +0 -0
  244. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/UniswapV3UniversalRouter.py +0 -0
  245. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_crosschain.py +0 -0
  246. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_foundry.py +0 -0
  247. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_guide.py +0 -0
  248. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_hypercore_model.py +0 -0
  249. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_hypercore_shadow.py +0 -0
  250. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/_simulate.py +0 -0
  251. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/addresses.py +0 -0
  252. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/constants.py +0 -0
  253. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  254. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/hypercore_market55.json +0 -0
  255. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/clean.json +0 -0
  256. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  257. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/rich.json +0 -0
  258. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  259. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_about.py +0 -0
  260. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cache.py +0 -0
  261. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_chains.py +0 -0
  262. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_changelog.py +0 -0
  263. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_config_store.py +0 -0
  264. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_market_morpho_blue.py +0 -0
  265. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_cli_oracle_mapping.py +0 -0
  266. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_quote.py +0 -0
  267. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_encoding.py +0 -0
  268. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_errors.py +0 -0
  269. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_layering.py +0 -0
  270. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_readiness_report.py +0 -0
  271. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_error_hierarchy.py +0 -0
  272. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_examples_composed_hypercore.py +0 -0
  273. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_examples_crosschain.py +0 -0
  274. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  275. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_explorer.py +0 -0
  276. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_external_state_events.py +0 -0
  277. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_fuse_encoding.py +0 -0
  278. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_fusion_instance.py +0 -0
  279. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_guide.py +0 -0
  280. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_events.py +0 -0
  281. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_outcome.py +0 -0
  282. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_logs.py +0 -0
  283. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_hypercore.py +0 -0
  284. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_models.py +0 -0
  285. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_readiness_selection.py +0 -0
  286. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_revert_decoding.py +0 -0
  287. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_roles.py +0 -0
  288. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_sdk_layering.py +0 -0
  289. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_canary.py +0 -0
  290. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  291. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_arbitrum.py +0 -0
  292. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_base.py +0 -0
  293. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_crosschain.py +0 -0
  294. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_solidity_compiler.py +0 -0
  295. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_substrates.py +0 -0
  296. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_types_and_events.py +0 -0
  297. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/tests/test_vault_dep_graph.py +0 -0
  298. {ipor_fusion-4.0.0.dev88 → ipor_fusion-4.0.0.dev103}/uv.lock +0 -0
@@ -188,8 +188,9 @@ the lifecycle matrix follows that.
188
188
  - `substrates.py` — public per-market bytes32 substrate decoding (`decode_substrate`,
189
189
  `SubstrateInfo`, `format_market_label`, `market_name`; re-exported from `ipor_fusion`),
190
190
  used by the CLI, MCP and `readers/lending_health`
191
- - `validation.py` — public vault validation (`validate_vault`; `read_erc20_holdings` +
192
- `validate_vault_state` for an already-fetched state typed by the `VaultState` protocol):
191
+ - `validation.py` — public vault validation (`validate_vault` fetches and validates;
192
+ `validate_vault_state` is a pure function of a snapshot typed by the `VaultState`
193
+ protocol):
193
194
  `VaultValidation` with ordered `Finding`s, an ok/warning/critical verdict, the
194
195
  `Reconciliation`, `Erc20Holdings` and lending health; `dependency_graph.py` holds the
195
196
  balance-graph checks
@@ -197,9 +198,11 @@ the lifecycle matrix follows that.
197
198
  `WithdrawManagerData`), `fetch_deployment_info`, cached `resolve_token_symbol` /
198
199
  `resolve_token_decimals`; it composes `vault_reads.py` (the shared Multicall3 batch
199
200
  and degrade-to-``None`` helpers), `vault_managers.py` (fee and WithdrawManager
200
- snapshots) and `vault_positions.py` (Morpho/Aave position breakdowns and their token
201
- prices); `vault_info.py` — `build_vault_info_json`, the
202
- `fusion vault info --json` document (contract names via an injected `resolve_name`),
201
+ snapshots), `vault_positions.py` (Morpho/Aave position breakdowns and their token
202
+ prices) and `vault_erc20.py` (the ERC20_VAULT_BALANCE holdings);
203
+ `vault_info.py` — `build_vault_info_json(data)`, the `fusion vault info --json`
204
+ document rendered from the snapshot alone (contract names via an injected
205
+ `resolve_name`),
203
206
  and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
204
207
  `cache.py` the on-disk contract and deployment caches (`~/.cache/ipor-fusion/`).
205
208
  `morpho_api.py` the Morpho public GraphQL API client (`fetch_market`, `fetch_vault`,
@@ -213,8 +216,8 @@ the lifecycle matrix follows that.
213
216
  time and request budget, and raises `LogScanError` rather than return partial logs;
214
217
  `LogSource`: an optional index `Web3Context.get_logs` asks first for a topic0-only
215
218
  filter, falling back to the scan on `LogSourceUnavailable`;
216
- prefer a storage or view read over a scan, as `get_balance_fuses` and
217
- `withdraw_manager_address` do), `plasma_vault`, `access`, `withdraw_manager`, `rewards_manager`, `fee_manager`,
219
+ prefer a storage or view read over a scan, as `read_balance_fuses` and
220
+ `read_withdraw_manager_address` do), `plasma_vault`, `access`, `withdraw_manager`, `rewards_manager`, `fee_manager`,
218
221
  `simulation` (`VaultSimulator`, eth_simulateV1, `erc20_balance_slot` and ERC-20 balance
219
222
  overrides), `oracle`, `fusion_factory`,
220
223
  `external_state_executor` (NAV marks for market 50), `erc20`, `fuse_action`
@@ -276,18 +279,36 @@ Two patterns explain most of the code; know both before writing SDK code
276
279
  or examples.
277
280
 
278
281
  - `Call[T]` (`core/contract.py`): every `ContractWrapper` method (one per
279
- Solidity function) returns a `Call` instead of executing. `.call()` runs
280
- `eth_call` and decodes to `T`; `.send()` signs and submits, returning a
281
- `TxReceipt`; `.calldata` hands the bytes to an external signer (a wrapper
282
- built with `ctx=None`, e.g. `PlasmaVault(None, address)`, builds calldata
283
- offline; there is no separate encoder factory);
284
- `.build_transaction()` / `Web3Context.estimate_gas` preview without signing.
285
- A `Call` without `output_types` (a write) raises on `.call()`. The same
286
- `Call` feeds `VaultSimulator.observe`, so reads, sends and simulations share
287
- one definition.
282
+ Solidity function) returns a `Call` instead of executing. Construction and
283
+ Call building are offline: a wrapper is its address (plus genuine contract
284
+ parameters such as a legacy oracle's decimals) and holds no `Web3Context`;
285
+ a `Call` holds `to`, `data`, output types and decoder, never a context.
286
+ Execution takes the context as a required argument: `.call(ctx)` runs
287
+ `eth_call` (at the context's, possibly pinned, block; `block=` overrides)
288
+ and decodes to `T`; `.send(ctx)` signs and submits, returning a `TxReceipt`;
289
+ `.build_transaction(ctx)` / `.estimate_gas(ctx)` preview without signing
290
+ (default `"latest"`). `.calldata`, `.decode(raw)` and `.map(f)` stay
291
+ offline. A `Call` without `output_types` (a write) raises on `.call(ctx)`.
292
+ The same `Call` feeds `VaultSimulator.observe`, so reads, sends and
293
+ simulations share one definition.
294
+ - A wrapper method that must execute to answer (storage, a log scan, a read
295
+ that depends on another read) is named `read_*` and takes `ctx` first:
296
+ `PlasmaVault.read_balance_fuses(ctx)`, `AccessManager.read_all_role_accounts(ctx)`,
297
+ `MorphoReader.read_rates(ctx, id)`. Decide by the I/O a method performs, not
298
+ by the class name. Resolvers that probe the chain to pick a wrapper
299
+ (`price_oracle_middleware(ctx, a)`, `resolve_access_manager`,
300
+ `ExternalStateExecutor.for_vault`) return context-free wrappers. Executors
301
+ and orchestrators own their contexts: `Multicall3(ctx)`, `VaultSimulator`,
302
+ `CrosschainSimulator`, `OracleMappingReader(ctx, oracle)` and
303
+ `CrosschainLane` (`hub_ctx` for the executor, `spoke_ctx` for the
304
+ dispatcher at the same address: run a lane's Calls on the matching one).
305
+ A chain-dependent decode takes the chain explicitly
306
+ (`ExternalStateExecutor.pending_proposals(account, chain_id=)`); a decoder
307
+ never consults the executing context.
288
308
  - Batch independent reads: `Multicall3(ctx).aggregate(calls)` (raises as
289
- `.call()` would) or `.try_aggregate(calls)` (``None`` per failed read) runs
290
- many `Call`s in one `eth_call` at one block. A loop of `.call()`s, or a
309
+ `.call(ctx)` would) or `.try_aggregate(calls)` (``None`` per failed read) runs
310
+ many `Call`s in one `eth_call` at one block; every batch, re-run and
311
+ no-Multicall fallback runs on its `ctx`. A loop of `.call(ctx)`s, or a
291
312
  thread pool of them, costs one round trip each; RPC latency, not decoding,
292
313
  is what makes the vault tooling slow.
293
314
  - Fuses (`fuses/`) are stateless encoders: a method returns an immutable
@@ -295,6 +316,9 @@ or examples.
295
316
  via `self._action_raw(solidity_signature, values)`; the shared `_validate_*`
296
317
  helpers raise before encoding. `PlasmaVault.execute([actions])` runs the
297
318
  batch atomically.
319
+ - Pin reads, never mutate a shared context: `ctx.at_block(n)` is a copy whose
320
+ reads (and `get_logs` upper bound) default to block ``n``; `Call.call(ctx, block=n)`
321
+ pins one read.
298
322
  - Simulate before sending: `VaultSimulator(web3, vault=, alpha=)` batches
299
323
  `execute` and `observe` calls into one `eth_simulateV1` round trip, across
300
324
  blocks via `next_block`. Integration tests and examples use it instead of a
@@ -383,9 +407,14 @@ are not validated.
383
407
 
384
408
  ## Domain rules (verified against the contracts)
385
409
 
386
- - `fetch_vault_data(..., chain_id=0)` (the default) skips lending_health,
387
- Morpho/Aave position breakdowns and token prices, all guarded by `if chain_id:`.
388
- Pass a real chain_id or those come back null/empty, silently.
410
+ - `fetch_vault_data(ctx, vault, *, block=, lending=, metadata=)` is the snapshot:
411
+ it resolves one block (``block``, else a numeric `ctx.default_block`, else latest),
412
+ runs every read on `ctx.at_block(block)` (log scans end there too) and returns
413
+ everything validation and the ``vault info`` document need, so neither reads the
414
+ chain. The chain is `ctx.chain_id`; ``lending=False`` skips lending health, the
415
+ position breakdowns, their prices and the unpriceable check; ``metadata=False``
416
+ skips token symbols/decimals. Add a new chain-state input to the snapshot, never a
417
+ read inside a renderer or validator.
389
418
  - PlasmaVault serves governance/view functions via `fallback()` delegatecall to
390
419
  PlasmaVaultBase, so Etherscan's ABI for the vault address omits callable
391
420
  view functions such as `getDependencyBalanceGraph`. An empty dependency graph
@@ -57,20 +57,40 @@ names earlier reachable only by module path (`FusionFactory`, `FusionInstance`,
57
57
  `ipor_fusion`: a module path outside the README's list (*Public API and stability*)
58
58
  still works but is internal.
59
59
 
60
- **Vault reads and validation.** `fetch_vault_data(chain_id=)`, `fetch_deployment_info`,
61
- `build_vault_info_json`, `ensure_supported_chain` and `fetch_vault_lending_health`
62
- take `ChainId`. The vault is a `PlasmaVaultAddress` (see *Address roles*) in
63
- `validate_vault` and `fetch_deployment_info`; `build_vault_info_json(ctx, plasma_vault,
64
- data, *, chain_id, chain_label)` takes no address, takes the chain by keyword, and
65
- labels the document with `plasma_vault.address` (a test that mocks `PlasmaVault`
66
- sets `.address`). `fetch_unpriceable_priced_tokens` takes the oracle and
60
+ **Vault reads and validation.** The vault read is one pinned snapshot, and the
61
+ chain is always the context's:
62
+
63
+ - `fetch_vault_data(ctx, vault, *, block=None, lending=True, metadata=True)` takes
64
+ the vault address (not a `PlasmaVault`) and no `chain_id`. It resolves one block
65
+ (``block``, else a numeric `ctx.default_block`, else the latest) and runs every
66
+ read, log scans included, on `ctx.at_block(block)`; `ctx` is not modified. The old
67
+ ``chain_id=0`` switch is ``lending=False``; ``metadata=False`` skips token symbols
68
+ and decimals. `VaultData` gains `vault`, `chain_id`, `market_balances`, `erc20`,
69
+ `token_symbols` and `token_decimals`; `WithdrawManagerData` gains
70
+ `shares_to_release_assets` and `pending_request_assets`.
71
+ - `validate_vault(ctx, vault, *, block=None)` fetches with ``metadata=False`` and
72
+ validates; it no longer takes ``resolve_symbol``. `validate_vault_state(state)` is
73
+ pure: a custom `VaultState` also provides `vault`, `chain_id`, `market_balances`,
74
+ `erc20` and the queue's `shares_to_release_assets`. `read_erc20_holdings` is gone:
75
+ the holdings are `VaultData.erc20`.
76
+ - `build_vault_info_json(data, *, resolve_name=None)` renders from the snapshot and
77
+ reads nothing; the chain and its label come from `data.chain_id`.
78
+ - `fetch_deployment_info(ctx, vault, *, api_key=None)` and
79
+ `fetch_vault_lending_health(ctx, vault, *, balance_fuse_market_ids,
80
+ market_substrates, aave_pools=None)` take the chain from `ctx`.
81
+ - `Web3Context(web3, chain_id, *, ...)` and `Web3Context.from_url(url, *, ...)` take
82
+ their options by keyword. New: `Web3Context.at_block(block)` (a pinned copy) and
83
+ `Call.call(ctx, *, block=None)` (see [Context-free wrappers](#context-free-wrappers)). `Web3Context.get_logs(to_block=None)` ends at
84
+ the context's default block and `Web3Context.get_block(block=None)` returns it,
85
+ both instead of ``"latest"``.
86
+
87
+ `ensure_supported_chain` takes `ChainId`. `fetch_unpriceable_priced_tokens` takes the oracle and
67
88
  underlying as `ChecksumAddress`. `resolve_token_symbol`, `resolve_token_decimals`,
68
89
  `get_contract_name` and `get_deployment_tx` take `HexAddress` (a checksum address
69
90
  fits), and the `resolve_name` / `resolve_symbol` callbacks receive one (a callback
70
91
  typed for `str` still fits). Market-keyed data is keyed by `MarketId`:
71
92
  `VaultData.market_substrates`, `dependency_graph`, `morpho_positions`,
72
- `aave_positions`; the `VaultState` protocol, `validate_vault_state`
73
- (`market_balances`), `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens`
93
+ `aave_positions`, `market_balances`; the `VaultState` protocol, `fetch_vault_lending_health`, `fetch_unpriceable_priced_tokens`
74
94
  and the `dependency_graph` helpers (`compute_update_reach`, `compute_update_groups`,
75
95
  `find_orphan_fuse_markets`, `find_markets_missing_erc20_dependency`).
76
96
 
@@ -93,7 +113,7 @@ The Uniswap V3 / Ramses V2 position events' `version`, `sender` and `recipient`
93
113
  `quote_ccip_native_fee(block=)` and `OracleMappingReader.asset_source_events` take a
94
114
  `BlockIdentifier`.
95
115
 
96
- **Address roles.** `PlasmaVault(ctx, address)` (`ctx` may be `None`, see below),
116
+ **Address roles.** `PlasmaVault(address)`,
97
117
  `validate_vault`, `fetch_deployment_info`, the HyperCore, lending-health and
98
118
  oracle-mapping readers, `read_external_state`, `VaultSimulator(vault=)`,
99
119
  `CrosschainSimulator`, crosschain discovery, the hub-manager inputs
@@ -110,7 +130,7 @@ Every fuse constructor, `FuseAction.fuse`, `BalanceFuse.fuse`,
110
130
  `LaneFuses` and the HyperCore balance-fuse argument take a `FuseAddress`, and
111
131
  `get_fuses()`, `get_instant_withdrawal_fuses()`, `get_rewards_fuses()` and
112
132
  `VaultData.fuses` / `instant_fuses` / `fuse_markets` keys return it.
113
- `AccessManager(ctx, address)` takes an `AccessManagerAddress`;
133
+ `AccessManager(address)` takes an `AccessManagerAddress`;
114
134
  `get_access_manager_address()`, `VaultData.access_manager` and
115
135
  `FusionInstance.access_manager` return one. `resolve_access_manager` still takes any
116
136
  `ChecksumAddress`: it is what checks that the address is a vault.
@@ -147,7 +167,7 @@ gone: use `morpho_blue_address(chain_id)`.
147
167
  `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
148
168
 
149
169
  **Access.** `AccessManager.has_role`, `grant_role`, `revoke_role`,
150
- `get_accounts_with_role` take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
170
+ `read_accounts_with_role` take `RoleId | Roles`; `RoleAccount.role_id` is `RoleId`.
151
171
 
152
172
  **Morpho.** `MorphoBlueMarketId` now always carries `0x`; build it with
153
173
  `parse_morpho_market_id` instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market`,
@@ -194,29 +214,6 @@ are `ProposalHash`; `ExternalStateEvent.transaction_hash` is a `TxHash`.
194
214
  except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
195
215
  `MarketId`. Calling an MCP tool function directly from Python takes `ChainId`.
196
216
 
197
- **Offline wrappers.** `ContractWrapper.encoder()` (and the `PlasmaVault` /
198
- `AccessManager` overrides) is gone. Every wrapper constructor takes
199
- `ctx: Web3Context | None`, so build an offline wrapper with the normal
200
- constructor and an address: `PlasmaVault(None, vault)`, `ERC20(None, token)`,
201
- `FusionFactory(None, factory)`, `LegacyPriceOracleMiddleware(None, oracle,
202
- Decimals(8))`. The address is required (there is no zero-address placeholder) and
203
- subclass parameters stay required, so the wrapper is fully initialized:
204
- `.encoder()` skipped the constructor, and a legacy oracle built that way failed to
205
- decode `get_asset_price`. `.calldata` still needs no context; `.call()`, `.send()`,
206
- `.build_transaction()` and `.estimate_gas()` raise `ValueError` until one is passed
207
- to them, and a method that reads the chain itself (log scans such as
208
- `get_balance_fuses` or `get_pending_requests`, `withdraw_manager_address`,
209
- `Multicall3.aggregate` / `try_aggregate`, the Aave V3 and Morpho position reads)
210
- raises `ValueError("Web3Context required: <wrapper> at <address> ...")` instead of
211
- an `AttributeError` on `None`. `ExternalStateExecutor` still refuses the zero
212
- address for its hash and event readers.
213
-
214
- | 3.x | 4.0 |
215
- |---|---|
216
- | `PlasmaVault.encoder(vault)` | `PlasmaVault(None, vault)` |
217
- | `ERC20.encoder()` (no address) | `ERC20(None, token)`: pass the real address |
218
- | `Cls.__new__(Cls)` + `_ctx` / `_address` | `Cls(None, address)` |
219
-
220
217
  **Fuse base.** `Fuse`, `StakeFuse` and the shared validators and substrate packers
221
218
  live in `ipor_fusion.core.fuse`; `ipor_fusion.fuses.base`, `ipor_fusion.fuses` and
222
219
  `ipor_fusion` re-export them, so existing imports keep working. Fuse equality and
@@ -229,6 +226,93 @@ hash now cover every attribute a fuse stores, not only its class and address (se
229
226
  `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
230
227
  `vault_markets_view` and `oracle_mapping_view` (private names, not API).
231
228
 
229
+ ## Context-free wrappers
230
+
231
+ A contract wrapper is its address (plus genuine contract parameters, such as a legacy
232
+ oracle's decimals) and nothing chain-bound. Construction and `Call` building are
233
+ offline; the `Web3Context` is a required argument where something executes. In 3.x
234
+ the context was stored on the wrapper and copied into every `Call`; that is gone,
235
+ together with `ContractWrapper.encoder()`, `Call.ctx` and the zero-address
236
+ placeholder wrapper.
237
+
238
+ - Build every wrapper from its address: `PlasmaVault(vault)`, `ERC20(token)`,
239
+ `FusionFactory(factory)`, `AccessManager(manager)`,
240
+ `LegacyPriceOracleMiddleware(oracle, Decimals(8))`, the crosschain executor,
241
+ dispatcher and factory wrappers, the Chainlink CCIP wrappers, the readers
242
+ (`MorphoReader(morpho)`, `AaveV3Reader(pool)`, ...). `HyperCoreReader()` takes no
243
+ argument. The nominal address types are unchanged.
244
+ - Execute a `Call` on a context: `.call(ctx, *, block=None)` (reads default to the
245
+ context's, possibly pinned, block), `.send(ctx)`, `.build_transaction(ctx, *,
246
+ block="latest")`, `.estimate_gas(ctx, *, block="latest")`. `ctx` may be passed by
247
+ keyword. Without it Python raises `TypeError` (and pyright reports the call),
248
+ instead of the 3.x `ValueError("Web3Context required ...")`. `.calldata`,
249
+ `.decode(raw)` and `.map(f)` need no context. `Call(...)` no longer takes `ctx=`.
250
+ - A wrapper method that executes to answer (storage, log scans, dependent reads) is
251
+ named `read_*` and takes the context first (table below).
252
+ - Executors and orchestrators keep their contexts: `Multicall3(ctx)` (every batch,
253
+ every re-run of a failed sub-call and the per-call fallback run on its `ctx`, so a
254
+ pinned context keeps them all at its block; it is no longer a `ContractWrapper`,
255
+ `aggregate3(calls)` still returns the raw `Call`), `VaultSimulator`,
256
+ `CrosschainSimulator` and `OracleMappingReader(ctx, oracle)`.
257
+ - Resolvers that probe the chain stay eager and return context-free wrappers:
258
+ `price_oracle_middleware(ctx, oracle)`, `resolve_access_manager(ctx, vault)`,
259
+ `ExternalStateExecutor.for_vault(ctx, vault)`. A resolved oracle carries the ABI
260
+ found on that chain and block; resolve again for another one.
261
+ - Crosschain lanes own both contexts: `CcipLane` / `StargateLane` take `hub_ctx=` and
262
+ `spoke_ctx=` instead of `spoke_chain_id=` (it is `spoke_ctx.chain_id`), and expose
263
+ them as `lane.hub_ctx` / `lane.spoke_ctx`. `needs_attestation`,
264
+ `command_in_flight` and `transfer_in_flight` read on `hub_ctx`. The Calls a lane
265
+ returns are unexecuted, and the executor and dispatcher share one address, so pick
266
+ the context yourself: `lane.get_balance().call(lane.hub_ctx)`,
267
+ `lane.observation().call(lane.spoke_ctx)`. `CrosschainTransport.executor(address)`
268
+ and `.dispatcher(address)` no longer take a context.
269
+ - Chain-bound decoding takes the chain explicitly:
270
+ `ExternalStateExecutor.pending_proposals(account, *, chain_id)`,
271
+ `parse_balance_proposed(log, *, chain_id)` and
272
+ `find_balance_proposed(logs, account=None, *, chain_id)` require it (3.x fell back
273
+ to the wrapper's context). The decoder captures it, so `.call(ctx)`, `Multicall3`
274
+ and `VaultSimulator.observe` stamp the same hash for the same bytes, whatever
275
+ context executes them. Pass `ctx.chain_id` for the chain you read from.
276
+ `pending_proposals` now refuses an executor at the zero address when the Call is
277
+ built, not when it is decoded. `mark_nav` takes the chain from its contexts.
278
+
279
+ | 3.x | 4.0 |
280
+ |---|---|
281
+ | `PlasmaVault(ctx, vault)`, `ERC20(ctx, token)`, any `Wrapper(ctx, address, ...)` | `PlasmaVault(vault)`, `ERC20(token)`, `Wrapper(address, ...)` |
282
+ | `PlasmaVault.encoder(vault)`, `ERC20.encoder()` | `PlasmaVault(vault)`, `ERC20(token)` (the real address) |
283
+ | `LegacyPriceOracleMiddleware(ctx, oracle, decimals)` | `LegacyPriceOracleMiddleware(oracle, decimals)` |
284
+ | `HyperCoreReader(ctx)` | `HyperCoreReader()` |
285
+ | `Call(to=, data=, ..., ctx=ctx)` | `Call(to=, data=, ...)` |
286
+ | `call.call()` / `call.call(block=n)` | `call.call(ctx)` / `call.call(ctx, block=n)` |
287
+ | `call.send()` | `call.send(ctx)` |
288
+ | `call.build_transaction(block=b)`, `call.estimate_gas(block=b)` | `call.build_transaction(ctx, block=b)`, `call.estimate_gas(ctx, block=b)` |
289
+ | `call.ctx` | gone: keep the context yourself |
290
+ | `PlasmaVault.get_balance_fuses()` | `PlasmaVault.read_balance_fuses(ctx)` |
291
+ | `PlasmaVault.withdraw_manager_address()` | `PlasmaVault.read_withdraw_manager_address(ctx)` |
292
+ | `PlasmaVault.price_oracle_address()` | `PlasmaVault.read_price_oracle_address(ctx)` |
293
+ | `AccessManager.owner()` / `owners()` / `atomists()` | `read_owner(ctx)` / `read_owners(ctx)` / `read_atomists(ctx)` |
294
+ | `AccessManager.get_accounts_with_role(role)` | `AccessManager.read_accounts_with_role(ctx, role)` |
295
+ | `AccessManager.get_all_role_accounts()` | `AccessManager.read_all_role_accounts(ctx)` |
296
+ | `PriceOracleMiddleware.get_assets_price_sources()` | `PriceOracleMiddleware.read_assets_price_sources(ctx)` |
297
+ | `WithdrawManager.get_pending_requests(from_block)` | `WithdrawManager.read_pending_requests(ctx, from_block)` |
298
+ | `WithdrawManager.get_pending_requests_info(from_block)` | `WithdrawManager.read_pending_requests_info(ctx, from_block)` |
299
+ | `MorphoReader.position_breakdown(id, user)` | `MorphoReader.read_position_breakdown(ctx, id, user)` |
300
+ | `MorphoReader.require_market(id)` | `MorphoReader.read_existing_market(ctx, id)` |
301
+ | `MorphoReader.rates(id)` | `MorphoReader.read_rates(ctx, id)` |
302
+ | `MorphoReader.rates_from(market, params)` | `MorphoReader.read_rates_from(ctx, market, params)` (it reads the IRM) |
303
+ | `AaveV3Reader.position_breakdown(asset, user)` | `AaveV3Reader.read_position_breakdown(ctx, asset, user)` |
304
+ | `AaveV3FuseReader.pool()` | `AaveV3FuseReader.read_pool(ctx)` |
305
+ | `executor.pending_proposals(account)` | `executor.pending_proposals(account, chain_id=ctx.chain_id)` |
306
+ | `executor.parse_balance_proposed(log)` / `find_balance_proposed(logs)` | `...(log, chain_id=...)` / `...(logs, chain_id=...)` |
307
+ | `CcipLane(executor=, dispatcher=, spoke_chain_id=, ...)` | `CcipLane(hub_ctx=, spoke_ctx=, executor=, dispatcher=, ...)` (same for `StargateLane`) |
308
+ | `transport.executor(ctx, address)` / `transport.dispatcher(ctx, address)` | `transport.executor(address)` / `transport.dispatcher(address)` |
309
+
310
+ A service that caches wrappers per chain can now share one wrapper across chains and
311
+ snapshots; what it must not share is a `Call` result decoded with a chain-bound
312
+ argument (`chain_id=`) for another chain. An external signer still needs the real
313
+ target address (`call.to`) and builds the chain-qualified transaction itself, or
314
+ gets it from `call.build_transaction(ctx)`.
315
+
232
316
  ## Behavior changes
233
317
 
234
318
  - Two fuses are equal (and hash alike) only when they have the same class and the
@@ -305,7 +389,7 @@ is the wrong one.
305
389
  executor of a transport. An executor reporting `transportKind()` `UNDEFINED` is now
306
390
  rejected by `detect_transport` instead of being returned. A transport kind passed in
307
391
  that has no lane is still a `ValueError`.
308
- - `Call.call()` and `Call.decode()` on a write-only `Call` raise `ValueError`, not
392
+ - `Call.call(ctx)` and `Call.decode()` on a write-only `Call` raise `ValueError`, not
309
393
  `RuntimeError`. `CrosschainSimulator.relay` raises `ValueError`, not
310
394
  `RuntimeError`, for a message to a chain that was not added.
311
395
  - `VaultSimulator.run` (provider error from `eth_simulateV1`, or a result count that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev88
3
+ Version: 4.0.0.dev103
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
@@ -141,8 +141,8 @@ ctx = Web3Context.from_url(
141
141
  private_key="0x...",
142
142
  )
143
143
 
144
- # 2. Wrap the PlasmaVault contract
145
- vault = PlasmaVault(ctx, Web3.to_checksum_address("0xVAULT_ADDRESS"))
144
+ # 2. Wrap the PlasmaVault contract (an address only; contexts are passed at execution)
145
+ vault = PlasmaVault(Web3.to_checksum_address("0xVAULT_ADDRESS"))
146
146
 
147
147
  # 3. Build a fuse action (e.g. supply USDC to Aave V3)
148
148
  fuse = AaveV3SupplyFuse(Web3.to_checksum_address("0xFUSE_ADDRESS"))
@@ -151,25 +151,28 @@ action = fuse.supply(
151
151
  amount=1_000_000, # 1 USDC (6 decimals)
152
152
  )
153
153
 
154
- # 4. Execute on-chain (execute() returns a Call; .send() signs and submits it)
155
- receipt = vault.execute([action]).send()
154
+ # 4. Execute on-chain (execute() returns a Call; .send(ctx) signs and submits it)
155
+ receipt = vault.execute([action]).send(ctx)
156
156
  ```
157
157
 
158
158
  Fuse, factory and manager addresses per chain are published in
159
159
  [ipor-abi](https://github.com/IPOR-Labs/ipor-abi) (`mainnet/mainnet-<chain>-fusion/addresses.json`).
160
- A fuse must also be registered on the vault; `vault.get_fuses().call()` lists the registered ones.
160
+ A fuse must also be registered on the vault; `vault.get_fuses().call(ctx)` lists the registered ones.
161
161
 
162
162
  Amounts are raw on-chain integers (`Amount`, `Shares` in `ipor_fusion.types`); the SDK never scales by decimals.
163
163
 
164
164
  ### Read, send, or simulate
165
165
 
166
- Every wrapper method returns a `Call` instead of executing. The same `Call` powers all three modes:
166
+ Every wrapper method returns a `Call` instead of executing. Wrappers and `Call`s hold no
167
+ `Web3Context`: the context is a required argument wherever something runs, and methods that
168
+ must read the chain to answer are named `read_*` and take it first
169
+ (`vault.read_balance_fuses(ctx)`). The same `Call` powers all three modes:
167
170
 
168
171
  ```python
169
172
  from ipor_fusion import VaultSimulator
170
173
 
171
- total = vault.total_assets().call() # eth_call -> Amount
172
- receipt = vault.execute([action]).send() # signed tx -> TxReceipt
174
+ total = vault.total_assets().call(ctx) # eth_call -> Amount
175
+ receipt = vault.execute([action]).send(ctx) # signed tx -> TxReceipt
173
176
  payload = vault.execute([action]).calldata # raw bytes for an external signer
174
177
 
175
178
  # Simulate first via eth_simulateV1 (no local node); alpha is the account allowed to call execute()
@@ -312,7 +315,7 @@ Reverts on the deploy-and-configure path, keyed by selector so a failed transact
312
315
  | `0x9996b315` | `AddressEmptyCode(address)` | `execute()` touched a market with no balance fuse | `add_balance_fuse(market_id, balance_fuse)` before the first `execute` on that market |
313
316
  | `0x4bf4de4e` | `HandlerNotFound()` | A protocol called back into the vault during `execute()` (a flash loan) and no handler is registered for its address and selector | `update_callback_handler(handler, sender, selector)` before the first `execute` that triggers the callback |
314
317
  | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | `AccessManager.grant_role(role, account, 0)` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
315
- | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send()` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
318
+ | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send(ctx)` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
316
319
 
317
320
  `clone()` grants the owner only `Roles.OWNER_ROLE` (1). OWNER grants `Roles.ATOMIST_ROLE` (100), which administers `Roles.FUSE_MANAGER_ROLE` (300, configuration), `Roles.ALPHA_ROLE` (200, `execute`) and `Roles.WHITELIST_ROLE` (800, `deposit` on a private vault).
318
321
 
@@ -398,10 +401,10 @@ The SDK uses a **fuse adapter pattern**:
398
401
  - **Fuses** encode protocol-specific calls into `FuseAction` objects (pure calldata, no state)
399
402
  - **PlasmaVault** batches and executes `FuseAction` sequences on-chain via `execute()`; a batch is atomic
400
403
  - **Web3Context** manages provider connections, signing, and transaction dispatch
401
- - **Call** is the lazy result of every wrapper method: `.call()`, `.send()`, `.calldata`, or feed it to `VaultSimulator`
404
+ - **Call** is the lazy result of every wrapper method: `.call(ctx)`, `.send(ctx)`, `.calldata`, or feed it to `VaultSimulator`
402
405
 
403
406
  ```
404
- Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call --> .send() / simulate
407
+ Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call --> .send(ctx) / simulate
405
408
  ```
406
409
 
407
410
  ### Core modules (`ipor_fusion.core`)
@@ -409,7 +412,7 @@ Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call
409
412
  | Module | Purpose |
410
413
  |--------|---------|
411
414
  | `Web3Context` | Provider connection, signing, tx dispatch, gas estimation |
412
- | `Call` | Pre-encoded contract call; `.call()`, `.send()`, `.calldata`, `.build_transaction()` |
415
+ | `Call` | Pre-encoded contract call; `.call(ctx)`, `.send(ctx)`, `.calldata`, `.build_transaction(ctx)` |
413
416
  | `PlasmaVault` | ERC-4626 vault — execute, deposit, withdraw, fuse and market configuration |
414
417
  | `VaultSimulator` | Batch `execute` + reads through `eth_simulateV1`, multi-block, no local node |
415
418
  | `FusionFactory` | Deploy a new vault (`clone`, `clone_supervised`) |
@@ -93,8 +93,8 @@ ctx = Web3Context.from_url(
93
93
  private_key="0x...",
94
94
  )
95
95
 
96
- # 2. Wrap the PlasmaVault contract
97
- vault = PlasmaVault(ctx, Web3.to_checksum_address("0xVAULT_ADDRESS"))
96
+ # 2. Wrap the PlasmaVault contract (an address only; contexts are passed at execution)
97
+ vault = PlasmaVault(Web3.to_checksum_address("0xVAULT_ADDRESS"))
98
98
 
99
99
  # 3. Build a fuse action (e.g. supply USDC to Aave V3)
100
100
  fuse = AaveV3SupplyFuse(Web3.to_checksum_address("0xFUSE_ADDRESS"))
@@ -103,25 +103,28 @@ action = fuse.supply(
103
103
  amount=1_000_000, # 1 USDC (6 decimals)
104
104
  )
105
105
 
106
- # 4. Execute on-chain (execute() returns a Call; .send() signs and submits it)
107
- receipt = vault.execute([action]).send()
106
+ # 4. Execute on-chain (execute() returns a Call; .send(ctx) signs and submits it)
107
+ receipt = vault.execute([action]).send(ctx)
108
108
  ```
109
109
 
110
110
  Fuse, factory and manager addresses per chain are published in
111
111
  [ipor-abi](https://github.com/IPOR-Labs/ipor-abi) (`mainnet/mainnet-<chain>-fusion/addresses.json`).
112
- A fuse must also be registered on the vault; `vault.get_fuses().call()` lists the registered ones.
112
+ A fuse must also be registered on the vault; `vault.get_fuses().call(ctx)` lists the registered ones.
113
113
 
114
114
  Amounts are raw on-chain integers (`Amount`, `Shares` in `ipor_fusion.types`); the SDK never scales by decimals.
115
115
 
116
116
  ### Read, send, or simulate
117
117
 
118
- Every wrapper method returns a `Call` instead of executing. The same `Call` powers all three modes:
118
+ Every wrapper method returns a `Call` instead of executing. Wrappers and `Call`s hold no
119
+ `Web3Context`: the context is a required argument wherever something runs, and methods that
120
+ must read the chain to answer are named `read_*` and take it first
121
+ (`vault.read_balance_fuses(ctx)`). The same `Call` powers all three modes:
119
122
 
120
123
  ```python
121
124
  from ipor_fusion import VaultSimulator
122
125
 
123
- total = vault.total_assets().call() # eth_call -> Amount
124
- receipt = vault.execute([action]).send() # signed tx -> TxReceipt
126
+ total = vault.total_assets().call(ctx) # eth_call -> Amount
127
+ receipt = vault.execute([action]).send(ctx) # signed tx -> TxReceipt
125
128
  payload = vault.execute([action]).calldata # raw bytes for an external signer
126
129
 
127
130
  # Simulate first via eth_simulateV1 (no local node); alpha is the account allowed to call execute()
@@ -264,7 +267,7 @@ Reverts on the deploy-and-configure path, keyed by selector so a failed transact
264
267
  | `0x9996b315` | `AddressEmptyCode(address)` | `execute()` touched a market with no balance fuse | `add_balance_fuse(market_id, balance_fuse)` before the first `execute` on that market |
265
268
  | `0x4bf4de4e` | `HandlerNotFound()` | A protocol called back into the vault during `execute()` (a flash loan) and no handler is registered for its address and selector | `update_callback_handler(handler, sender, selector)` before the first `execute` that triggers the callback |
266
269
  | `0x068ca9d8` | `AccessManagedUnauthorized(address)` | The caller lacks the role the function requires: `FUSE_MANAGER` for `add_fuses`, `grant_market_substrates`, `add_balance_fuse`, `update_callback_handler`; `ALPHA` for `execute`; `WHITELIST` for `deposit` and `mint` on a private vault | `AccessManager.grant_role(role, account, 0)` from the role's admin (`OWNER` grants `ATOMIST`, `ATOMIST` grants the rest); for a reverting `deposit`, whitelist the depositor or convert the vault to public |
267
- | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send()` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
270
+ | `ValueError: Private key required for sending transactions` | SDK, before any transaction | `.send(ctx)` on a `Web3Context` without a key | `Web3Context(w3, chain_id, signer=..., private_key=...)` or `Web3Context.from_url(url, private_key=...)` |
268
271
 
269
272
  `clone()` grants the owner only `Roles.OWNER_ROLE` (1). OWNER grants `Roles.ATOMIST_ROLE` (100), which administers `Roles.FUSE_MANAGER_ROLE` (300, configuration), `Roles.ALPHA_ROLE` (200, `execute`) and `Roles.WHITELIST_ROLE` (800, `deposit` on a private vault).
270
273
 
@@ -350,10 +353,10 @@ The SDK uses a **fuse adapter pattern**:
350
353
  - **Fuses** encode protocol-specific calls into `FuseAction` objects (pure calldata, no state)
351
354
  - **PlasmaVault** batches and executes `FuseAction` sequences on-chain via `execute()`; a batch is atomic
352
355
  - **Web3Context** manages provider connections, signing, and transaction dispatch
353
- - **Call** is the lazy result of every wrapper method: `.call()`, `.send()`, `.calldata`, or feed it to `VaultSimulator`
356
+ - **Call** is the lazy result of every wrapper method: `.call(ctx)`, `.send(ctx)`, `.calldata`, or feed it to `VaultSimulator`
354
357
 
355
358
  ```
356
- Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call --> .send() / simulate
359
+ Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call --> .send(ctx) / simulate
357
360
  ```
358
361
 
359
362
  ### Core modules (`ipor_fusion.core`)
@@ -361,7 +364,7 @@ Fuse.method() --> FuseAction --> PlasmaVault.execute([actions]) --> Call
361
364
  | Module | Purpose |
362
365
  |--------|---------|
363
366
  | `Web3Context` | Provider connection, signing, tx dispatch, gas estimation |
364
- | `Call` | Pre-encoded contract call; `.call()`, `.send()`, `.calldata`, `.build_transaction()` |
367
+ | `Call` | Pre-encoded contract call; `.call(ctx)`, `.send(ctx)`, `.calldata`, `.build_transaction(ctx)` |
365
368
  | `PlasmaVault` | ERC-4626 vault — execute, deposit, withdraw, fuse and market configuration |
366
369
  | `VaultSimulator` | Batch `execute` + reads through `eth_simulateV1`, multi-block, no local node |
367
370
  | `FusionFactory` | Deploy a new vault (`clone`, `clone_supervised`) |
@@ -35,7 +35,7 @@ own examples.
35
35
  The default (and only) behavior is preview plus `eth_simulateV1` simulation. An example must not
36
36
  sign or broadcast a transaction, and must not require a private key to run.
37
37
 
38
- - Show the read-only path with `.call()`, the unsigned-payload path with `.calldata`, and simulate
38
+ - Show the read-only path with `.call(ctx)`, the unsigned-payload path with `.calldata`, and simulate
39
39
  the whole flow with `VaultSimulator`.
40
40
  - For "how to actually submit this on-chain", point the reader at their signer via `.calldata`, and
41
41
  at the deployment/operation repos -- do not add a runnable broadcast path.
@@ -104,7 +104,7 @@ default run also logs the unsigned `clone(...)` creation calldata.
104
104
 
105
105
  These examples only simulate; they never broadcast. To submit a flow on-chain, take
106
106
  the `.calldata` each builder produces and sign it with your own signer -- a multisig,
107
- hardware wallet, or key-management service. (`Call.send()` performs a direct send from a
107
+ hardware wallet, or key-management service. (`Call.send(ctx)` performs a direct send from a
108
108
  configured signer, but a production deployment should not sign with a hot key.) Always
109
109
  review production configuration independently before using it with real funds.
110
110
 
@@ -6,7 +6,7 @@ collateralized borrow. It shows, end to end and without reading the test suite,
6
6
  how to:
7
7
 
8
8
  1. connect a ``Web3Context`` to Base;
9
- 2. preview a vault deployment with ``FusionFactory.clone(...).call()`` and read
9
+ 2. preview a vault deployment with ``FusionFactory.clone(...).call(ctx)`` and read
10
10
  its state-dependent CREATE addresses for the atomic simulation, plus the
11
11
  unsigned creation ``.calldata``;
12
12
  3. bootstrap the vault's roles in the correct order;
@@ -222,7 +222,7 @@ def _euler_account(plasma_vault: ChecksumAddress) -> ChecksumAddress:
222
222
 
223
223
 
224
224
  def _evault_debt_of(
225
- ctx: Web3Context, euler_vault: ChecksumAddress, account: ChecksumAddress
225
+ euler_vault: ChecksumAddress, account: ChecksumAddress
226
226
  ) -> Call[int]:
227
227
  """Read an Euler eVault's outstanding debt for an account (underlying units).
228
228
 
@@ -234,7 +234,7 @@ def _evault_debt_of(
234
234
  data = function_signature_to_4byte_selector("debtOf(address)") + encode(
235
235
  ["address"], [account]
236
236
  )
237
- return Call(to=euler_vault, data=data, output_types=["uint256"], ctx=ctx)
237
+ return Call(to=euler_vault, data=data, output_types=["uint256"])
238
238
 
239
239
 
240
240
  # ── Pure builders (no chain access -- safe to import and unit-test offline) ───
@@ -261,12 +261,12 @@ def clone_args() -> dict:
261
261
  def unsigned_clone_calldata() -> bytes:
262
262
  """Unsigned creation calldata (selector + ABI-encoded args) for the deploy.
263
263
 
264
- Built on a wrapper with ctx=None, so it needs no provider. Hand these bytes to
265
- an external signer / multisig instead of calling ``.send()``. NOTE: the owner
264
+ Built from an address-only wrapper, so it needs no provider. Hand these bytes to
265
+ an external signer / multisig instead of calling ``.send(ctx)``. NOTE: the owner
266
266
  encoded here is the simulation ``OWNER`` (a throwaway EOA) -- set your real
267
267
  owner in ``clone_args()`` before deploying, or the vault is owned by it.
268
268
  """
269
- return FusionFactory(None, BASE_FUSION_FACTORY).clone(**clone_args()).calldata
269
+ return FusionFactory(BASE_FUSION_FACTORY).clone(**clone_args()).calldata
270
270
 
271
271
 
272
272
  def market_substrates() -> list[bytes]:
@@ -369,10 +369,10 @@ def run_simulation(web3: Web3) -> SimulationResult:
369
369
  """Build, fund and run the full Euler V2 credit lifecycle in one batch."""
370
370
  ctx = Web3Context(web3=web3, chain_id=BASE_CHAIN_ID, signer=OWNER)
371
371
  ctx.default_block = PINNED_BLOCK
372
- factory = FusionFactory(ctx, BASE_FUSION_FACTORY)
372
+ factory = FusionFactory(BASE_FUSION_FACTORY)
373
373
 
374
374
  # 1. Preview the deterministic addresses (eth_call, no gas, no state change).
375
- preview = factory.clone(**clone_args()).call()
375
+ preview = factory.clone(**clone_args()).call(ctx)
376
376
  log.info(
377
377
  "predicted vault=%s access_manager=%s price_manager=%s (index=%d)",
378
378
  preview.plasma_vault,
@@ -383,19 +383,19 @@ def run_simulation(web3: Web3) -> SimulationResult:
383
383
  # 2. The unsigned creation calldata (external-signer path).
384
384
  log.info("unsigned clone calldata: 0x%s", unsigned_clone_calldata().hex())
385
385
 
386
- plasma_vault = PlasmaVault(ctx, preview.plasma_vault)
387
- access_manager = AccessManager(ctx, preview.access_manager)
388
- price_manager = PriceOracleMiddlewareManager(ctx, preview.price_manager)
389
- cbeth = ERC20(ctx, BASE_CBETH)
386
+ plasma_vault = PlasmaVault(preview.plasma_vault)
387
+ access_manager = AccessManager(preview.access_manager)
388
+ price_manager = PriceOracleMiddlewareManager(preview.price_manager)
389
+ cbeth = ERC20(BASE_CBETH)
390
390
  euler_account = _euler_account(preview.plasma_vault)
391
391
 
392
392
  # The whole flow runs as ONE eth_simulateV1 batch; nothing here is signed or
393
393
  # broadcast (see the module docstring). Each simulator primitive stands in for
394
394
  # a production action, read as "simulation -> production":
395
- # - sim.add_call(call=X, from_=OWNER) -> X.send() by the owner/atomist
395
+ # - sim.add_call(call=X, from_=OWNER) -> X.send(ctx) by the owner/atomist
396
396
  # (or X.calldata to a multisig / timelock).
397
- # - sim.execute([action]) -> plasma_vault.execute([action]).send() by the alpha.
398
- # - sim.observe("key", X) -> X.call(), a plain read.
397
+ # - sim.execute([action]) -> plasma_vault.execute([action]).send(ctx) by the alpha.
398
+ # - sim.observe("key", X) -> X.call(ctx), a plain read.
399
399
  sim = VaultSimulator(
400
400
  web3=web3, vault=preview.plasma_vault, alpha=ALPHA, block=PINNED_BLOCK
401
401
  )
@@ -499,7 +499,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
499
499
 
500
500
  # Borrow WETH against the cbETH collateral; debt is now open.
501
501
  sim.execute([borrow_action(BORROW_AMOUNT)])
502
- sim.observe("debt_after_borrow", _evault_debt_of(ctx, EVAULT_WETH, euler_account))
502
+ sim.observe("debt_after_borrow", _evault_debt_of(EVAULT_WETH, euler_account))
503
503
  plan.append("borrow 1 WETH against the cbETH collateral")
504
504
 
505
505
  # Re-value the market while the position is open so the balance fuse's
@@ -512,7 +512,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
512
512
  # advance in this run, interest is negligible and the borrowed WETH covers it.
513
513
  # (Advancing time would accrue interest, requiring a small extra WETH buffer.)
514
514
  sim.execute([repay_action(Amount(MAX_UINT256))])
515
- sim.observe("debt_after_repay", _evault_debt_of(ctx, EVAULT_WETH, euler_account))
515
+ sim.observe("debt_after_repay", _evault_debt_of(EVAULT_WETH, euler_account))
516
516
  plan.append("repay the full WETH debt")
517
517
 
518
518
  # Unwind in reverse of setup: disable the WETH controller, then the cbETH