ipor-fusion 4.0.0.dev93__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.dev93 → ipor_fusion-4.0.0.dev103}/AGENTS.md +32 -14
  2. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/MIGRATION_4.md +92 -28
  3. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/PKG-INFO +16 -13
  4. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/README.md +15 -12
  5. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/CONTRIBUTING.md +1 -1
  6. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/README.md +1 -1
  7. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/advanced_euler_v2_credit_market_base.py +17 -17
  8. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/advanced_euler_v2_looping_arbitrum.py +12 -15
  9. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +23 -25
  10. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +4 -4
  11. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/external_state_margin_leg_base.py +21 -21
  12. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/examples/simple_aave_v3_supply_base.py +13 -13
  13. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/pyproject.toml +4 -2
  14. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/scripts/crosschain_readiness.py +22 -21
  15. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/skills/ipor-deploy-vault/SKILL.md +10 -10
  16. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/market_cmd.py +3 -3
  17. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_cmd.py +6 -6
  18. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/access.py +26 -24
  19. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/contract.py +37 -64
  20. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/erc20.py +1 -1
  21. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/external_state_executor.py +30 -53
  22. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/multicall.py +37 -22
  23. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/oracle.py +16 -13
  24. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/plasma_vault.py +35 -30
  25. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/rewards_manager.py +1 -1
  26. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/simulation.py +1 -1
  27. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/withdraw_manager.py +16 -11
  28. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/chainlink.py +10 -12
  29. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/contracts.py +3 -3
  30. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/lane.py +22 -14
  31. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/transport.py +4 -9
  32. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/discovery.py +17 -18
  33. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/lane.py +26 -11
  34. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/contracts.py +2 -2
  35. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/lane.py +7 -6
  36. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/transport.py +4 -9
  37. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/transport.py +2 -7
  38. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/errors.py +1 -1
  39. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/architecture.md +4 -4
  40. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/invariants.md +3 -3
  41. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/quickstart.md +7 -7
  42. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/server.py +6 -6
  43. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/aave_v3.py +10 -11
  44. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/hypercore.py +6 -10
  45. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/hypercore_outcome.py +5 -5
  46. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/lending_health.py +9 -7
  47. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/morpho.py +21 -18
  48. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping.py +4 -4
  49. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping_reader.py +24 -25
  50. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/unpriceable.py +4 -6
  51. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_data.py +16 -17
  52. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_erc20.py +8 -4
  53. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_managers.py +6 -5
  54. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_positions.py +8 -10
  55. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_reads.py +5 -5
  56. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_crosschain_lifecycle.py +10 -10
  57. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_crosschain_recovery.py +1 -1
  58. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_euler_v2.py +14 -18
  59. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_multicall.py +2 -2
  60. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_access_manager.py +24 -23
  61. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_commands.py +20 -20
  62. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_helpers.py +11 -12
  63. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_lending_health.py +7 -7
  64. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_pending_requests_scan.py +3 -3
  65. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_role_accounts.py +9 -9
  66. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_info_golden.py +4 -4
  67. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_predates_getters.py +2 -2
  68. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_contract_call.py +15 -21
  69. ipor_fusion-4.0.0.dev103/tests/test_contract_encoder.py +158 -0
  70. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_events.py +2 -2
  71. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_indexer.py +1 -1
  72. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_chainlink.py +15 -13
  73. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_contracts.py +28 -27
  74. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_lane.py +125 -20
  75. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_readiness.py +11 -13
  76. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_transport.py +4 -8
  77. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_erc20.py +9 -9
  78. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_examples_vaults.py +2 -2
  79. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_external_state_executor.py +191 -187
  80. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_fee_manager.py +12 -12
  81. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_deployment.py +5 -5
  82. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_flow.py +3 -3
  83. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_live.py +17 -15
  84. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_readers.py +7 -9
  85. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_simulation.py +2 -2
  86. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_server.py +12 -10
  87. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_multicall.py +48 -3
  88. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_oracle.py +65 -23
  89. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_oracle_mapping.py +3 -3
  90. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_plasma_vault.py +62 -60
  91. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_reader_encoding.py +22 -22
  92. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_rewards_manager.py +12 -12
  93. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_borrow_aave_v3_mainnet.py +11 -15
  94. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_claim_merkl_wrapper_base.py +8 -12
  95. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_claim_morpho_rewards.py +3 -5
  96. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_hypercore.py +4 -6
  97. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_hyperevm.py +63 -62
  98. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_batch_base.py +5 -5
  99. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_credit_market_base.py +4 -8
  100. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_deploy_base.py +6 -6
  101. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +4 -4
  102. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_euler_v2_swap_registry_base.py +5 -5
  103. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_external_state_executor_base.py +17 -7
  104. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_fusion_factory_clone_base.py +12 -16
  105. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_looping_aave_v3_base.py +12 -14
  106. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_looping_morpho_blue_base.py +9 -11
  107. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_morpho_blue.py +5 -5
  108. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v3.py +34 -42
  109. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v4_uniswap.py +26 -38
  110. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v5_ramses.py +18 -26
  111. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_pilot_v5_scheduled.py +30 -42
  112. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_plasma_vault_setters_base.py +4 -5
  113. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_readers_integration.py +26 -24
  114. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_release_funds.py +11 -15
  115. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_swap_on_base.py +9 -11
  116. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_vault_from_scratch_base.py +5 -5
  117. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_solidity_mirrors.py +1 -1
  118. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_unpriceable_tokens.py +1 -1
  119. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +3 -3
  120. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_vault_simulator_blocks.py +1 -1
  121. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_web3_context.py +11 -11
  122. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_withdraw_manager.py +25 -27
  123. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/typed_client/consumer.py +69 -28
  124. ipor_fusion-4.0.0.dev103/tests/typed_client/rejected.py +51 -0
  125. ipor_fusion-4.0.0.dev93/tests/test_contract_encoder.py +0 -118
  126. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.claude-plugin/marketplace.json +0 -0
  127. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.claude-plugin/plugin.json +0 -0
  128. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.env.example +0 -0
  129. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.gemini/settings.json +0 -0
  130. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.git-blame-ignore-revs +0 -0
  131. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/cd.yml +0 -0
  132. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/ci.yml +0 -0
  133. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/commit-lint.yml +0 -0
  134. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/notify-slack.yml +0 -0
  135. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/python-build.yml +0 -0
  136. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/python-publish.yml +0 -0
  137. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/release.yml +0 -0
  138. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.github/workflows/report-slack.yml +0 -0
  139. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.gitignore +0 -0
  140. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.mcp.json +0 -0
  141. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.pre-commit-config.yaml +0 -0
  142. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/.python-version +0 -0
  143. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/CHANGELOG.md +0 -0
  144. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/CLAUDE.md +0 -0
  145. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/CODEOWNERS +0 -0
  146. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/LICENSE +0 -0
  147. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/build.sh +0 -0
  148. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/pytest.ini +0 -0
  149. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/scripts/commit-check.sh +0 -0
  150. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/__init__.py +0 -0
  151. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/about.py +0 -0
  152. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cache.py +0 -0
  153. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/chains.py +0 -0
  154. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/__init__.py +0 -0
  155. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  156. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/config_cmd.py +0 -0
  157. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/config_store.py +0 -0
  158. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/main.py +0 -0
  159. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/options.py +0 -0
  160. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  161. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_health.py +0 -0
  162. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  163. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  164. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  165. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/config/__init__.py +0 -0
  166. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/config/roles.py +0 -0
  167. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/__init__.py +0 -0
  168. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/address.py +0 -0
  169. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/context.py +0 -0
  170. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fee_manager.py +0 -0
  171. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fuse.py +0 -0
  172. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fuse_action.py +0 -0
  173. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/fusion_factory.py +0 -0
  174. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/core/logs.py +0 -0
  175. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/__init__.py +0 -0
  176. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  177. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  178. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  179. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  180. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  181. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/contracts.py +0 -0
  182. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/errors.py +0 -0
  183. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/logs.py +0 -0
  184. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/messages.py +0 -0
  185. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/simulation.py +0 -0
  186. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  187. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  188. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/dependency_graph.py +0 -0
  189. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/entry_cli.py +0 -0
  190. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/entry_mcp.py +0 -0
  191. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/explorer.py +0 -0
  192. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/field_docs.py +0 -0
  193. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/__init__.py +0 -0
  194. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  195. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/async_action.py +0 -0
  196. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/base.py +0 -0
  197. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  198. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/__init__.py +0 -0
  199. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/base.py +0 -0
  200. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/ccip.py +0 -0
  201. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/crosschain/stargate.py +0 -0
  202. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/erc4626.py +0 -0
  203. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  204. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/events.py +0 -0
  205. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/external_state.py +0 -0
  206. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/external_state_events.py +0 -0
  207. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  208. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  209. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/hypercore.py +0 -0
  210. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  211. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/merkl.py +0 -0
  212. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/morpho.py +0 -0
  213. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  214. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  215. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/fuses/universal.py +0 -0
  216. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/__init__.py +0 -0
  217. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/guide/glossary.md +0 -0
  218. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/market_ids.py +0 -0
  219. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/market_info.py +0 -0
  220. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/__init__.py +0 -0
  221. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/guide.py +0 -0
  222. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/mcp/models.py +0 -0
  223. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/morpho_api.py +0 -0
  224. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/py.typed +0 -0
  225. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/__init__.py +0 -0
  226. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/aave_v4.py +0 -0
  227. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/compound_v3.py +0 -0
  228. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  229. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/position_manager.py +0 -0
  230. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  231. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  232. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/substrates.py +0 -0
  233. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/types.py +0 -0
  234. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/validation.py +0 -0
  235. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/src/ipor_fusion/vault_info.py +0 -0
  236. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/UniswapV3UniversalRouter.py +0 -0
  237. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_crosschain.py +0 -0
  238. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_foundry.py +0 -0
  239. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_guide.py +0 -0
  240. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_hypercore_model.py +0 -0
  241. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_hypercore_shadow.py +0 -0
  242. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_simulate.py +0 -0
  243. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/_validation.py +0 -0
  244. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/addresses.py +0 -0
  245. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/conftest.py +0 -0
  246. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/constants.py +0 -0
  247. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  248. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/hypercore_market55.json +0 -0
  249. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/clean.json +0 -0
  250. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  251. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/rich.json +0 -0
  252. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  253. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_about.py +0 -0
  254. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cache.py +0 -0
  255. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_chains.py +0 -0
  256. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_changelog.py +0 -0
  257. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_config_store.py +0 -0
  258. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_hypercore.py +0 -0
  259. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_market_morpho_blue.py +0 -0
  260. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_oracle_mapping.py +0 -0
  261. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_cli_vault_validate.py +0 -0
  262. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_ccip_quote.py +0 -0
  263. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_encoding.py +0 -0
  264. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_errors.py +0 -0
  265. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_layering.py +0 -0
  266. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_crosschain_readiness_report.py +0 -0
  267. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_error_hierarchy.py +0 -0
  268. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_examples_composed_hypercore.py +0 -0
  269. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_examples_crosschain.py +0 -0
  270. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  271. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_explorer.py +0 -0
  272. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_external_state_events.py +0 -0
  273. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_fuse_encoding.py +0 -0
  274. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_fusion_instance.py +0 -0
  275. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_guide.py +0 -0
  276. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_events.py +0 -0
  277. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_hypercore_outcome.py +0 -0
  278. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_logs.py +0 -0
  279. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_hypercore.py +0 -0
  280. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_models.py +0 -0
  281. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_mcp_vault_validate.py +0 -0
  282. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_public_api.py +0 -0
  283. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_readiness_selection.py +0 -0
  284. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_revert_decoding.py +0 -0
  285. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_roles.py +0 -0
  286. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_sdk_layering.py +0 -0
  287. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_canary.py +0 -0
  288. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  289. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_arbitrum.py +0 -0
  290. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_base.py +0 -0
  291. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_simulate_examples_crosschain.py +0 -0
  292. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_solidity_compiler.py +0 -0
  293. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_substrates.py +0 -0
  294. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_types_and_events.py +0 -0
  295. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_validation.py +0 -0
  296. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_vault_dep_graph.py +0 -0
  297. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/tests/test_vault_info_onchain_base.py +0 -0
  298. {ipor_fusion-4.0.0.dev93 → ipor_fusion-4.0.0.dev103}/uv.lock +0 -0
@@ -216,8 +216,8 @@ the lifecycle matrix follows that.
216
216
  time and request budget, and raises `LogScanError` rather than return partial logs;
217
217
  `LogSource`: an optional index `Web3Context.get_logs` asks first for a topic0-only
218
218
  filter, falling back to the scan on `LogSourceUnavailable`;
219
- prefer a storage or view read over a scan, as `get_balance_fuses` and
220
- `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`,
221
221
  `simulation` (`VaultSimulator`, eth_simulateV1, `erc20_balance_slot` and ERC-20 balance
222
222
  overrides), `oracle`, `fusion_factory`,
223
223
  `external_state_executor` (NAV marks for market 50), `erc20`, `fuse_action`
@@ -279,18 +279,36 @@ Two patterns explain most of the code; know both before writing SDK code
279
279
  or examples.
280
280
 
281
281
  - `Call[T]` (`core/contract.py`): every `ContractWrapper` method (one per
282
- Solidity function) returns a `Call` instead of executing. `.call()` runs
283
- `eth_call` and decodes to `T`; `.send()` signs and submits, returning a
284
- `TxReceipt`; `.calldata` hands the bytes to an external signer (a wrapper
285
- built with `ctx=None`, e.g. `PlasmaVault(None, address)`, builds calldata
286
- offline; there is no separate encoder factory);
287
- `.build_transaction()` / `Web3Context.estimate_gas` preview without signing.
288
- A `Call` without `output_types` (a write) raises on `.call()`. The same
289
- `Call` feeds `VaultSimulator.observe`, so reads, sends and simulations share
290
- 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.
291
308
  - Batch independent reads: `Multicall3(ctx).aggregate(calls)` (raises as
292
- `.call()` would) or `.try_aggregate(calls)` (``None`` per failed read) runs
293
- 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
294
312
  thread pool of them, costs one round trip each; RPC latency, not decoding,
295
313
  is what makes the vault tooling slow.
296
314
  - Fuses (`fuses/`) are stateless encoders: a method returns an immutable
@@ -299,7 +317,7 @@ or examples.
299
317
  helpers raise before encoding. `PlasmaVault.execute([actions])` runs the
300
318
  batch atomically.
301
319
  - Pin reads, never mutate a shared context: `ctx.at_block(n)` is a copy whose
302
- reads (and `get_logs` upper bound) default to block ``n``; `Call.call(block=n)`
320
+ reads (and `get_logs` upper bound) default to block ``n``; `Call.call(ctx, block=n)`
303
321
  pins one read.
304
322
  - Simulate before sending: `VaultSimulator(web3, vault=, alpha=)` batches
305
323
  `execute` and `observe` calls into one `eth_simulateV1` round trip, across
@@ -80,7 +80,7 @@ chain is always the context's:
80
80
  market_substrates, aave_pools=None)` take the chain from `ctx`.
81
81
  - `Web3Context(web3, chain_id, *, ...)` and `Web3Context.from_url(url, *, ...)` take
82
82
  their options by keyword. New: `Web3Context.at_block(block)` (a pinned copy) and
83
- `Call.call(ctx=None, *, block=None)`. `Web3Context.get_logs(to_block=None)` ends at
83
+ `Call.call(ctx, *, block=None)` (see [Context-free wrappers](#context-free-wrappers)). `Web3Context.get_logs(to_block=None)` ends at
84
84
  the context's default block and `Web3Context.get_block(block=None)` returns it,
85
85
  both instead of ``"latest"``.
86
86
 
@@ -113,7 +113,7 @@ The Uniswap V3 / Ramses V2 position events' `version`, `sender` and `recipient`
113
113
  `quote_ccip_native_fee(block=)` and `OracleMappingReader.asset_source_events` take a
114
114
  `BlockIdentifier`.
115
115
 
116
- **Address roles.** `PlasmaVault(ctx, address)` (`ctx` may be `None`, see below),
116
+ **Address roles.** `PlasmaVault(address)`,
117
117
  `validate_vault`, `fetch_deployment_info`, the HyperCore, lending-health and
118
118
  oracle-mapping readers, `read_external_state`, `VaultSimulator(vault=)`,
119
119
  `CrosschainSimulator`, crosschain discovery, the hub-manager inputs
@@ -130,7 +130,7 @@ Every fuse constructor, `FuseAction.fuse`, `BalanceFuse.fuse`,
130
130
  `LaneFuses` and the HyperCore balance-fuse argument take a `FuseAddress`, and
131
131
  `get_fuses()`, `get_instant_withdrawal_fuses()`, `get_rewards_fuses()` and
132
132
  `VaultData.fuses` / `instant_fuses` / `fuse_markets` keys return it.
133
- `AccessManager(ctx, address)` takes an `AccessManagerAddress`;
133
+ `AccessManager(address)` takes an `AccessManagerAddress`;
134
134
  `get_access_manager_address()`, `VaultData.access_manager` and
135
135
  `FusionInstance.access_manager` return one. `resolve_access_manager` still takes any
136
136
  `ChecksumAddress`: it is what checks that the address is a vault.
@@ -167,7 +167,7 @@ gone: use `morpho_blue_address(chain_id)`.
167
167
  `decode_substrate`, `market_name`, `format_market_label` take `MarketId`.
168
168
 
169
169
  **Access.** `AccessManager.has_role`, `grant_role`, `revoke_role`,
170
- `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`.
171
171
 
172
172
  **Morpho.** `MorphoBlueMarketId` now always carries `0x`; build it with
173
173
  `parse_morpho_market_id` instead of `MorphoBlueMarketId("<64 hex>")`. `fetch_market`,
@@ -214,29 +214,6 @@ are `ProposalHash`; `ExternalStateEvent.transaction_hash` is a `TxHash`.
214
214
  except below): `chain_id` fields are `ChainId`, `role_id` `RoleId`, `market_id`
215
215
  `MarketId`. Calling an MCP tool function directly from Python takes `ChainId`.
216
216
 
217
- **Offline wrappers.** `ContractWrapper.encoder()` (and the `PlasmaVault` /
218
- `AccessManager` overrides) is gone. Every wrapper constructor takes
219
- `ctx: Web3Context | None`, so build an offline wrapper with the normal
220
- constructor and an address: `PlasmaVault(None, vault)`, `ERC20(None, token)`,
221
- `FusionFactory(None, factory)`, `LegacyPriceOracleMiddleware(None, oracle,
222
- Decimals(8))`. The address is required (there is no zero-address placeholder) and
223
- subclass parameters stay required, so the wrapper is fully initialized:
224
- `.encoder()` skipped the constructor, and a legacy oracle built that way failed to
225
- decode `get_asset_price`. `.calldata` still needs no context; `.call()`, `.send()`,
226
- `.build_transaction()` and `.estimate_gas()` raise `ValueError` until one is passed
227
- to them, and a method that reads the chain itself (log scans such as
228
- `get_balance_fuses` or `get_pending_requests`, `withdraw_manager_address`,
229
- `Multicall3.aggregate` / `try_aggregate`, the Aave V3 and Morpho position reads)
230
- raises `ValueError("Web3Context required: <wrapper> at <address> ...")` instead of
231
- an `AttributeError` on `None`. `ExternalStateExecutor` still refuses the zero
232
- address for its hash and event readers.
233
-
234
- | 3.x | 4.0 |
235
- |---|---|
236
- | `PlasmaVault.encoder(vault)` | `PlasmaVault(None, vault)` |
237
- | `ERC20.encoder()` (no address) | `ERC20(None, token)`: pass the real address |
238
- | `Cls.__new__(Cls)` + `_ctx` / `_address` | `Cls(None, address)` |
239
-
240
217
  **Fuse base.** `Fuse`, `StakeFuse` and the shared validators and substrate packers
241
218
  live in `ipor_fusion.core.fuse`; `ipor_fusion.fuses.base`, `ipor_fusion.fuses` and
242
219
  `ipor_fusion` re-export them, so existing imports keep working. Fuse equality and
@@ -249,6 +226,93 @@ hash now cover every attribute a fuse stores, not only its class and address (se
249
226
  `ipor_fusion.cli.vault_cmd` was split into `options`, `vault_info_view`,
250
227
  `vault_markets_view` and `oracle_mapping_view` (private names, not API).
251
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
+
252
316
  ## Behavior changes
253
317
 
254
318
  - Two fuses are equal (and hash alike) only when they have the same class and the
@@ -325,7 +389,7 @@ is the wrong one.
325
389
  executor of a transport. An executor reporting `transportKind()` `UNDEFINED` is now
326
390
  rejected by `detect_transport` instead of being returned. A transport kind passed in
327
391
  that has no lane is still a `ValueError`.
328
- - `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
329
393
  `RuntimeError`. `CrosschainSimulator.relay` raises `ValueError`, not
330
394
  `RuntimeError`, for a message to a chain that was not added.
331
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.dev93
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
@@ -183,14 +183,13 @@ def _euler_account(plasma_vault: ChecksumAddress) -> ChecksumAddress:
183
183
 
184
184
 
185
185
  def _debt_of(
186
- ctx: Web3Context,
187
186
  contract: ChecksumAddress,
188
187
  account: ChecksumAddress,
189
188
  ) -> Call[int]:
190
189
  data = function_signature_to_4byte_selector("debtOf(address)") + encode(
191
190
  ["address"], [account]
192
191
  )
193
- return Call(to=contract, data=data, output_types=["uint256"], ctx=ctx)
192
+ return Call(to=contract, data=data, output_types=["uint256"])
194
193
 
195
194
 
196
195
  # ── Pure builders (no chain access) ──────────────────────────────────────────
@@ -208,7 +207,7 @@ def clone_args() -> dict:
208
207
 
209
208
 
210
209
  def unsigned_clone_calldata() -> bytes:
211
- return FusionFactory(None, ARBITRUM_FUSION_FACTORY).clone(**clone_args()).calldata
210
+ return FusionFactory(ARBITRUM_FUSION_FACTORY).clone(**clone_args()).calldata
212
211
 
213
212
 
214
213
  def euler_market_substrates() -> list[bytes]:
@@ -260,9 +259,7 @@ def enable_euler_actions() -> list[FuseAction]:
260
259
  def build_swap_action() -> FuseAction:
261
260
  """Swap the borrowed USDC through Uniswap V3 and require flash repayment."""
262
261
  transfer_data = (
263
- ERC20(None, ARBITRUM_USDC)
264
- .transfer(UNISWAP_UNIVERSAL_ROUTER, BORROW_USDC)
265
- .calldata
262
+ ERC20(ARBITRUM_USDC).transfer(UNISWAP_UNIVERSAL_ROUTER, BORROW_USDC).calldata
266
263
  )
267
264
  path = encode_packed(
268
265
  ["address", "uint24", "address"],
@@ -320,8 +317,8 @@ def run_simulation(web3: Web3) -> SimulationResult:
320
317
  """Build, configure and run the WBTC/USDC leveraged loop without broadcasting."""
321
318
  ctx = Web3Context(web3=web3, chain_id=ARBITRUM_CHAIN_ID, signer=OWNER)
322
319
  ctx.default_block = PINNED_BLOCK
323
- factory = FusionFactory(ctx, ARBITRUM_FUSION_FACTORY)
324
- preview = factory.clone(**clone_args()).call()
320
+ factory = FusionFactory(ARBITRUM_FUSION_FACTORY)
321
+ preview = factory.clone(**clone_args()).call(ctx)
325
322
 
326
323
  log.info(
327
324
  "predicted vault=%s access_manager=%s price_manager=%s",
@@ -331,11 +328,11 @@ def run_simulation(web3: Web3) -> SimulationResult:
331
328
  )
332
329
  log.info("unsigned clone calldata: 0x%s", unsigned_clone_calldata().hex())
333
330
 
334
- vault = PlasmaVault(ctx, preview.plasma_vault)
335
- access_manager = AccessManager(ctx, preview.access_manager)
336
- price_manager = PriceOracleMiddlewareManager(ctx, preview.price_manager)
337
- wbtc = ERC20(ctx, ARBITRUM_WBTC)
338
- usdc = ERC20(ctx, ARBITRUM_USDC)
331
+ vault = PlasmaVault(preview.plasma_vault)
332
+ access_manager = AccessManager(preview.access_manager)
333
+ price_manager = PriceOracleMiddlewareManager(preview.price_manager)
334
+ wbtc = ERC20(ARBITRUM_WBTC)
335
+ usdc = ERC20(ARBITRUM_USDC)
339
336
  euler_account = _euler_account(preview.plasma_vault)
340
337
 
341
338
  sim = VaultSimulator(
@@ -427,11 +424,11 @@ def run_simulation(web3: Web3) -> SimulationResult:
427
424
  sim.add_call(call=vault.update_markets_balances([EULER_MARKET]), from_=ALPHA)
428
425
  sim.observe(
429
426
  "collateral_shares",
430
- ERC20(ctx, EVAULT_WBTC).balance_of(euler_account),
427
+ ERC20(EVAULT_WBTC).balance_of(euler_account),
431
428
  )
432
429
  sim.observe(
433
430
  "usdc_debt",
434
- _debt_of(ctx, EVAULT_USDC, euler_account),
431
+ _debt_of(EVAULT_USDC, euler_account),
435
432
  )
436
433
  sim.observe("vault_wbtc", wbtc.balance_of(preview.plasma_vault))
437
434
  sim.observe("vault_usdc", usdc.balance_of(preview.plasma_vault))