ipor-fusion 4.0.0.dev151__tar.gz → 4.0.0.dev156__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 (302) hide show
  1. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/AGENTS.md +36 -19
  2. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/MIGRATION_4.md +98 -0
  3. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/PKG-INFO +4 -4
  4. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/README.md +3 -3
  5. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/advanced_euler_v2_credit_market_base.py +26 -22
  6. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/advanced_euler_v2_looping_arbitrum.py +27 -29
  7. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +35 -34
  8. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +87 -92
  9. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/external_state_margin_leg_base.py +170 -96
  10. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/simple_aave_v3_supply_base.py +20 -20
  11. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/pyproject.toml +3 -2
  12. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/__init__.py +111 -2
  13. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/vault_cmd.py +34 -3
  14. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/vault_info_view.py +4 -3
  15. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/vault_markets_view.py +7 -7
  16. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/__init__.py +15 -2
  17. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/access.py +12 -1
  18. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/contract.py +10 -9
  19. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/external_state_executor.py +3 -4
  20. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/fusion_factory.py +34 -36
  21. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/multicall.py +115 -6
  22. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/simulation.py +90 -65
  23. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/events.py +5 -2
  24. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/indexer.py +22 -13
  25. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/lane.py +27 -19
  26. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/simulation.py +13 -6
  27. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/stargate/lane.py +1 -2
  28. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/stargate/transport.py +20 -17
  29. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/transport.py +2 -2
  30. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/errors.py +3 -2
  31. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/external_state_events.py +14 -9
  32. ipor_fusion-4.0.0.dev156/src/ipor_fusion/market_info.py +396 -0
  33. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/mcp/models.py +8 -6
  34. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/morpho_api.py +192 -132
  35. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/hypercore.py +43 -43
  36. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/hypercore_outcome.py +17 -26
  37. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/lending_health.py +10 -8
  38. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/morpho.py +6 -7
  39. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/oracle_mapping_reader.py +5 -2
  40. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/unpriceable.py +11 -12
  41. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_data.py +74 -94
  42. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_erc20.py +18 -19
  43. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_info.py +500 -90
  44. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_managers.py +47 -61
  45. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_reads.py +3 -24
  46. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_crosschain_lifecycle.py +114 -110
  47. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_crosschain_recovery.py +155 -92
  48. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_multicall.py +10 -2
  49. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/conftest.py +1 -1
  50. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cache.py +4 -1
  51. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_helpers.py +11 -47
  52. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_market_morpho_blue.py +8 -0
  53. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_pending_requests_scan.py +5 -12
  54. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_ccip_events.py +11 -0
  55. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_errors.py +0 -2
  56. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_transport.py +21 -6
  57. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_examples_vaults.py +0 -2
  58. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_external_state_events.py +9 -0
  59. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_external_state_executor.py +11 -18
  60. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_live.py +15 -21
  61. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_simulation.py +8 -8
  62. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_mcp_models.py +218 -112
  63. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_multicall.py +158 -0
  64. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_oracle.py +3 -3
  65. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_public_api.py +103 -0
  66. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_borrow_aave_v3_mainnet.py +20 -22
  67. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_claim_merkl_wrapper_base.py +12 -14
  68. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_claim_morpho_rewards.py +3 -4
  69. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_crosschain_canary.py +15 -14
  70. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_crosschain_hypercore.py +9 -9
  71. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_crosschain_hyperevm.py +164 -158
  72. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_euler_v2_batch_base.py +5 -6
  73. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_euler_v2_credit_market_base.py +5 -6
  74. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_euler_v2_swap_deploy_base.py +9 -13
  75. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +5 -6
  76. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_euler_v2_swap_registry_base.py +7 -11
  77. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_external_state_executor_base.py +10 -10
  78. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_looping_aave_v3_base.py +9 -10
  79. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_looping_morpho_blue_base.py +7 -8
  80. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_morpho_blue.py +8 -9
  81. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_pilot_v3.py +34 -38
  82. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_pilot_v4_uniswap.py +30 -35
  83. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_pilot_v5_ramses.py +24 -28
  84. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_pilot_v5_scheduled.py +45 -56
  85. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_release_funds.py +12 -14
  86. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_swap_on_base.py +16 -18
  87. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_vault_from_scratch_base.py +15 -16
  88. ipor_fusion-4.0.0.dev156/tests/test_simulation_observation.py +126 -0
  89. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_unpriceable_tokens.py +9 -0
  90. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_vault_simulator_blocks.py +25 -25
  91. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/typed_client/consumer.py +71 -3
  92. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/typed_client/rejected.py +26 -0
  93. ipor_fusion-4.0.0.dev151/src/ipor_fusion/market_info.py +0 -223
  94. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.claude-plugin/marketplace.json +0 -0
  95. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.claude-plugin/plugin.json +0 -0
  96. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.env.example +0 -0
  97. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.gemini/settings.json +0 -0
  98. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.git-blame-ignore-revs +0 -0
  99. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/cd.yml +0 -0
  100. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/ci.yml +0 -0
  101. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/commit-lint.yml +0 -0
  102. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/notify-slack.yml +0 -0
  103. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/python-build.yml +0 -0
  104. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/python-publish.yml +0 -0
  105. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/release.yml +0 -0
  106. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.github/workflows/report-slack.yml +0 -0
  107. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.gitignore +0 -0
  108. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.mcp.json +0 -0
  109. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.pre-commit-config.yaml +0 -0
  110. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/.python-version +0 -0
  111. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/CHANGELOG.md +0 -0
  112. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/CLAUDE.md +0 -0
  113. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/CODEOWNERS +0 -0
  114. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/LICENSE +0 -0
  115. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/build.sh +0 -0
  116. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/CONTRIBUTING.md +0 -0
  117. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/examples/README.md +0 -0
  118. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/pytest.ini +0 -0
  119. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/scripts/commit-check.sh +0 -0
  120. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/scripts/crosschain_readiness.py +0 -0
  121. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/skills/ipor-deploy-vault/SKILL.md +0 -0
  122. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/about.py +0 -0
  123. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cache.py +0 -0
  124. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/chains.py +0 -0
  125. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/__init__.py +0 -0
  126. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  127. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/config_cmd.py +0 -0
  128. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/config_store.py +0 -0
  129. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/main.py +0 -0
  130. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/market_cmd.py +0 -0
  131. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/options.py +0 -0
  132. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/oracle_mapping_view.py +0 -0
  133. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/vault_health.py +0 -0
  134. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  135. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/config/__init__.py +0 -0
  136. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/config/roles.py +0 -0
  137. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/address.py +0 -0
  138. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/context.py +0 -0
  139. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/erc20.py +0 -0
  140. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/fee_manager.py +0 -0
  141. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/fuse.py +0 -0
  142. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/fuse_action.py +0 -0
  143. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/logs.py +0 -0
  144. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/oracle.py +0 -0
  145. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/plasma_vault.py +0 -0
  146. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/rewards_manager.py +0 -0
  147. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  148. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/__init__.py +0 -0
  149. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  150. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  151. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  152. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  153. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  154. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  155. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  156. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/contracts.py +0 -0
  157. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/discovery.py +0 -0
  158. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/errors.py +0 -0
  159. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  160. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  161. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  162. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  163. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/logs.py +0 -0
  164. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/messages.py +0 -0
  165. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  166. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  167. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  168. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/dependency_graph.py +0 -0
  169. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/entry_cli.py +0 -0
  170. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/entry_mcp.py +0 -0
  171. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/explorer.py +0 -0
  172. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/field_docs.py +0 -0
  173. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/__init__.py +0 -0
  174. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  175. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/async_action.py +0 -0
  176. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/base.py +0 -0
  177. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  178. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/erc4626.py +0 -0
  179. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  180. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/events.py +0 -0
  181. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/external_state.py +0 -0
  182. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  183. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  184. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/hypercore.py +0 -0
  185. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/hypercore_events.py +0 -0
  186. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/merkl.py +0 -0
  187. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/morpho.py +0 -0
  188. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  189. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  190. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/fuses/universal.py +0 -0
  191. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/guide/__init__.py +0 -0
  192. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/guide/architecture.md +0 -0
  193. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/guide/glossary.md +0 -0
  194. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/guide/invariants.md +0 -0
  195. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/guide/quickstart.md +0 -0
  196. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/market_ids.py +0 -0
  197. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/mcp/__init__.py +0 -0
  198. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/mcp/guide.py +0 -0
  199. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/mcp/server.py +0 -0
  200. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/py.typed +0 -0
  201. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/__init__.py +0 -0
  202. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/aave_v3.py +0 -0
  203. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/aave_v4.py +0 -0
  204. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/compound_v3.py +0 -0
  205. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  206. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/oracle_mapping_nodes.py +0 -0
  207. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/position_manager.py +0 -0
  208. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  209. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  210. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/substrates.py +0 -0
  211. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/types.py +0 -0
  212. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/validation.py +0 -0
  213. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/src/ipor_fusion/vault_positions.py +0 -0
  214. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/UniswapV3UniversalRouter.py +0 -0
  215. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_crosschain.py +0 -0
  216. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_euler_v2.py +0 -0
  217. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_foundry.py +0 -0
  218. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_guide.py +0 -0
  219. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_hypercore_model.py +0 -0
  220. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_hypercore_shadow.py +0 -0
  221. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_simulate.py +0 -0
  222. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/_validation.py +0 -0
  223. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/addresses.py +0 -0
  224. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/constants.py +0 -0
  225. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  226. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/hypercore_market55.json +0 -0
  227. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/vault_info_golden/clean.json +0 -0
  228. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  229. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/vault_info_golden/rich.json +0 -0
  230. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  231. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_about.py +0 -0
  232. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_access_manager.py +0 -0
  233. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_chains.py +0 -0
  234. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_changelog.py +0 -0
  235. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_commands.py +0 -0
  236. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_config_store.py +0 -0
  237. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_hypercore.py +0 -0
  238. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_lending_health.py +0 -0
  239. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_oracle_mapping.py +0 -0
  240. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_role_accounts.py +0 -0
  241. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_vault_info_golden.py +0 -0
  242. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_vault_predates_getters.py +0 -0
  243. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_cli_vault_validate.py +0 -0
  244. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_contract_call.py +0 -0
  245. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_contract_encoder.py +0 -0
  246. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_ccip_indexer.py +0 -0
  247. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_ccip_quote.py +0 -0
  248. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_chainlink.py +0 -0
  249. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_contracts.py +0 -0
  250. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_encoding.py +0 -0
  251. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_lane.py +0 -0
  252. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_layering.py +0 -0
  253. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_readiness.py +0 -0
  254. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_crosschain_readiness_report.py +0 -0
  255. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_docs_typing.py +0 -0
  256. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_erc20.py +0 -0
  257. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_error_hierarchy.py +0 -0
  258. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_examples_composed_hypercore.py +0 -0
  259. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_examples_crosschain.py +0 -0
  260. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  261. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_explorer.py +0 -0
  262. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_fee_manager.py +0 -0
  263. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_fuse_encoding.py +0 -0
  264. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_fusion_instance.py +0 -0
  265. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_guide.py +0 -0
  266. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_deployment.py +0 -0
  267. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_events.py +0 -0
  268. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_flow.py +0 -0
  269. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_outcome.py +0 -0
  270. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_hypercore_readers.py +0 -0
  271. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_logs.py +0 -0
  272. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_mcp_hypercore.py +0 -0
  273. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_mcp_server.py +0 -0
  274. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_mcp_vault_validate.py +0 -0
  275. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_oracle_mapping.py +0 -0
  276. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_plasma_vault.py +0 -0
  277. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_public_type_reachability.py +0 -0
  278. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_reader_encoding.py +0 -0
  279. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_readiness_selection.py +0 -0
  280. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_revert_decoding.py +0 -0
  281. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_rewards_manager.py +0 -0
  282. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_roles.py +0 -0
  283. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_sdk_layering.py +0 -0
  284. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  285. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_examples_arbitrum.py +0 -0
  286. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_examples_base.py +0 -0
  287. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_examples_crosschain.py +0 -0
  288. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  289. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  290. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_simulate_readers_integration.py +0 -0
  291. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_solidity_compiler.py +0 -0
  292. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_solidity_mirrors.py +0 -0
  293. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_substrates.py +0 -0
  294. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_types_and_events.py +0 -0
  295. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_validation.py +0 -0
  296. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_vault_data_block.py +0 -0
  297. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_vault_dep_graph.py +0 -0
  298. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_vault_info_onchain_base.py +0 -0
  299. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  300. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_web3_context.py +0 -0
  301. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/tests/test_withdraw_manager.py +0 -0
  302. {ipor_fusion-4.0.0.dev151 → ipor_fusion-4.0.0.dev156}/uv.lock +0 -0
@@ -186,7 +186,7 @@ the lifecycle matrix follows that.
186
186
  | Python 3.12 runtime, 3.11 floor | `.python-version` + CI `python-version` default; `requires-python`, ruff `target-version`, pyright `pythonVersion` |
187
187
  | `IporFusionMarkets`, `Roles` | `market_ids.py`, `config/roles.py` mirror `IporFusionMarkets.sol`, `Roles.sol` in `ipor-fusion/contracts/libraries/`; drift-gated by `tests/test_solidity_mirrors.py` — bump its pinned ref in the same change that syncs the mirrors An id sourced from a contracts feature branch ahead of the pinned ref goes in the test's `_AHEAD_OF_UPSTREAM` allowlist and is removed when the ref catches up |
188
188
  | substrate decoders | `substrates.py` registry mirrors each market's `contracts/fuses/<protocol>/*SubstrateLib.sol` or `*FuseLib.sol` |
189
- | `vault_info` JSON shape | `build_vault_info_json` in `vault_info.py`, models in `mcp/models.py` (`extra="forbid"`), `_full_vault_info_dict` fixture in `test_mcp_models.py` |
189
+ | `vault_info` / `fusion market` JSON shapes | the builders and their TypedDicts (`VaultInfoJson`, `MorphoBlueMarketJson`, `MetaMorphoV1Json` / `MetaMorphoV2Json`) in `vault_info.py` / `market_info.py`, models in `mcp/models.py` (`extra="forbid"`, the published schema), the `VaultInfoJson`-typed `_full_vault_info_dict` fixture in `test_mcp_models.py` |
190
190
  | CLI command set | every CLI command has a matching tool in `mcp/server.py` (`changelog` maps to `server_info`) |
191
191
 
192
192
  ## Layout (`src/ipor_fusion/`)
@@ -207,14 +207,14 @@ the lifecycle matrix follows that.
207
207
  balance-graph checks
208
208
  - `vault_data.py` — public vault read: `fetch_vault_data` → `VaultData` (with `FeeData`,
209
209
  `WithdrawManagerData`), `fetch_deployment_info`, cached `resolve_token_symbol` /
210
- `resolve_token_decimals`; it composes `vault_reads.py` (the shared Multicall3 batch
211
- and degrade-to-``None`` helpers), `vault_managers.py` (fee and WithdrawManager
210
+ `resolve_token_decimals`; it composes `vault_reads.py` (the degrade-to-``None``
211
+ helpers), `vault_managers.py` (fee and WithdrawManager
212
212
  snapshots), `vault_positions.py` (Morpho/Aave position breakdowns and their token
213
213
  prices) and `vault_erc20.py` (the ERC20_VAULT_BALANCE holdings);
214
214
  `vault_info.py` — `build_vault_info_json(data)`, the `fusion vault info --json`
215
215
  document rendered from the snapshot alone (contract names via an injected
216
- `resolve_name`),
217
- and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
216
+ `resolve_name`) and typed block by block (`VaultInfoJson` and its nested
217
+ TypedDicts), and `health_check_lists`; `explorer.py` Etherscan V2 (single endpoint, needs API key);
218
218
  `cache.py` the `ContractCache` protocol (`get`/`put` of immutable facts keyed
219
219
  `CacheKey(chain_id, address, field)`), `FileContractCache` (one JSON file, atomic
220
220
  replace, corrupt/unwritable degrades to memory) and `NullCache`; every SDK use goes
@@ -223,8 +223,14 @@ the lifecycle matrix follows that.
223
223
  `$XDG_CACHE_HOME/ipor-fusion/`) or `get_contract_name(cache=...)`. Never cache an
224
224
  absence (no code, no answer): it can change at a later block.
225
225
  `morpho_api.py` the Morpho public GraphQL API client (`fetch_market`, `fetch_vault`,
226
- `PUBLIC_ALLOCATOR_ADDRESSES`); `market_info.py` — `build_morpho_blue_market_json`,
227
- `build_meta_morpho_json`, the `fusion market` JSON documents.
226
+ `PUBLIC_ALLOCATOR_ADDRESSES`; it narrows the GraphQL body from `object`);
227
+ `market_info.py` — `build_morpho_blue_market_json`, `build_meta_morpho_json`, the
228
+ `fusion market` JSON documents (`MorphoBlueMarketJson`, `MetaMorphoV1Json` /
229
+ `MetaMorphoV2Json`). A JSON document is a TypedDict (`NotRequired` for a key only
230
+ some entries carry), never `dict[str, Any]`; JSON parsed from the network or a
231
+ caller enters as `object` / `Mapping[str, object]` and is narrowed with
232
+ `isinstance`. These two modules skip `from __future__ import annotations`, so a
233
+ TypedDict's `__optional_keys__` is right at run time.
228
234
  None of them imports `cli/` (`tests/test_sdk_layering.py`)
229
235
  - `config/roles.py` — `Roles` IntEnum
230
236
  - `core/` — `context` (`Web3Context`), `contract` (`Call`, `ContractWrapper`),
@@ -334,10 +340,14 @@ or examples.
334
340
  never consults the executing context.
335
341
  - Batch independent reads: `Multicall3(ctx).aggregate(calls)` (raises as
336
342
  `.call(ctx)` would) or `.try_aggregate(calls)` (``None`` per failed read) runs
337
- many `Call`s in one `eth_call` at one block; every batch, re-run and
338
- no-Multicall fallback runs on its `ctx`. A loop of `.call(ctx)`s, or a
339
- thread pool of them, costs one round trip each; RPC latency, not decoding,
340
- is what makes the vault tooling slow.
343
+ many `Call`s of one type in one `eth_call` at one block. Reads of different
344
+ types go through `batch = Multicall3(ctx).batch()`: `batch.add(call)` returns a
345
+ `BatchValue[T]` (raises from `batch.run()` as `.call(ctx)` would),
346
+ `batch.try_add(call)` a `BatchValue[T | None]`, and after one `batch.run()`
347
+ each `.value` keeps its type; never collect mixed reads in a `list[Call[Any]]`
348
+ or unpack an `aggregate` of them. Every batch, re-run and no-Multicall fallback
349
+ runs on its `ctx`. A loop of `.call(ctx)`s, or a thread pool of them, costs one
350
+ round trip each; RPC latency, not decoding, is what makes the vault tooling slow.
341
351
  - Fuses (`fuses/`) are stateless encoders: a method returns an immutable
342
352
  `FuseAction(fuse_address, calldata)` and touches no chain. `Fuse` builds it
343
353
  via `self._action_raw(solidity_signature, values)`; the shared `_validate_*`
@@ -348,8 +358,11 @@ or examples.
348
358
  pins one read.
349
359
  - Simulate before sending: `VaultSimulator(web3, vault=, alpha=)` batches
350
360
  `execute` and `observe` calls into one `eth_simulateV1` round trip, across
351
- blocks via `next_block`. Integration tests and examples use it instead of a
352
- local node.
361
+ blocks via `next_block`. `observe(call, label=)` returns a
362
+ `SimulationObservation[T]`; after `run()` its `.value` is the typed read (it
363
+ raises when the read reverted), from the latest run, so a crosschain relay
364
+ round re-resolves it. Labels only name calls in `result.calls` and failures.
365
+ Integration tests and examples use it instead of a local node.
353
366
  - Amounts are raw on-chain integers; `types.py` `NewType`s (`Amount`, `Shares`,
354
367
  `MarketId`, ...) name the unit, and nothing scales by decimals.
355
368
  - Three address roles are nominal: `PlasmaVaultAddress`, `FuseAddress` and
@@ -431,12 +444,16 @@ value sets are `Literal` enums. The docstring keeps only what has no
431
444
  structured home: a one-line summary, behavioral notes, cost warnings. Do not
432
445
  repeat schema content as prose; it drifts.
433
446
 
434
- `mcp/models.py` forbids extra keys. Adding a top-level field to
435
- `build_vault_info_json` requires adding it to the matching model (with a default
436
- for back-compat) and to the `_full_vault_info_dict` fixture; `test_mcp_models.py`
437
- enforces the mirror. Conditionally shaped blocks (`withdraw_manager_details`,
438
- `substrates`, `dependency_graph`) are `dict[str, Any]`, so their inner keys
439
- are not validated.
447
+ `mcp/models.py` forbids extra keys and stays the source of the published JSON
448
+ schema. Adding a field to `build_vault_info_json` (or a `fusion market` document)
449
+ means adding it to its TypedDict, to the matching model (with a default for
450
+ back-compat) and to the `_full_vault_info_dict` fixture;
451
+ `TestTypedDocumentsMirrorModels` in `test_mcp_models.py` holds every TypedDict to
452
+ its model block by block. The model keeps `dict[str, Any]` where the schema is
453
+ deliberately loose (`substrates`, `deployment`, `share_price`, `dependency_graph`,
454
+ `position_breakdown`): the SDK types those blocks, the schema does not validate
455
+ their inner keys. The substrate entries stay open-ended on both sides
456
+ (`SubstrateEntryJson`, `dict[str, str | bool]`): each decoder adds its own fields.
440
457
 
441
458
  ## Domain rules (verified against the contracts)
442
459
 
@@ -54,6 +54,10 @@ before it is fixed.
54
54
  nominal types. Wrap literals and raw values, `ChainId(8453)`, `Period(0)`,
55
55
  `Amount(n)`, `parse_fuse_address(...)`; values the SDK returns already carry
56
56
  their type. See [Nominal types](#nominal-types).
57
+ 9. **Typed JSON documents.** `build_vault_info_json` and the `fusion market`
58
+ builders return TypedDicts (`VaultInfoJson`, ...); code annotating them
59
+ `dict[str, Any]` switches to the TypedDict or `Mapping[str, object]`. See
60
+ [Typed JSON documents](#typed-json-documents).
57
61
 
58
62
  Then read [Behavior changes](#behavior-changes): they apply to every caller,
59
63
  type-checked or not.
@@ -138,6 +142,56 @@ placeholder wrapper.
138
142
  | `executor.parse_balance_proposed(log)` / `find_balance_proposed(logs)` | `...(log, chain_id=...)` / `...(logs, chain_id=...)` |
139
143
  | `CcipLane(executor=, dispatcher=, spoke_chain_id=, ...)` | `CcipLane(hub_ctx=, spoke_ctx=, executor=, dispatcher=, ...)` (same for `StargateLane`) |
140
144
  | `transport.executor(ctx, address)` / `transport.dispatcher(ctx, address)` | `transport.executor(address)` / `transport.dispatcher(address)` |
145
+ | `sim.observe("label", call)` ... `result.get("label")` | `read = sim.observe(call)` ... `read.value` |
146
+ | `csim.observe(chain_id, "label", call)` ... `results[chain_id].get("label")` | `read = csim.observe(chain_id, call)` ... `read.value` |
147
+ | `result.observations`, `SimulatedCallResult.decoded` | gone: keep the observation, or `call.decode(bytes(r.return_data))` |
148
+ | `a, b = Multicall3(ctx).aggregate(cast(list[Call[Any]], [x, y]))` | `batch = Multicall3(ctx).batch()`; `a = batch.add(x)`; `b = batch.add(y)`; `batch.run()`; `a.value`, `b.value` |
149
+
150
+ ### Typed batches and simulation observations
151
+
152
+ Reads of different types keep their types. `Multicall3(ctx).batch()` returns a
153
+ `Multicall3Batch`: `batch.add(call)` queues a `Call[T]` and returns a
154
+ `BatchValue[T]`; `batch.try_add(call)` returns a `BatchValue[T | None]`. One
155
+ `batch.run()` executes every queued read in one `eth_call` per `batch_size` reads
156
+ (one per read where Multicall3 is not deployed), exactly as `aggregate` does for the
157
+ same calls. A failing `add` read raises from `run()` what `Call.call(ctx)` would; a
158
+ failing `try_add` read is `None`, as in `try_aggregate`. Each read chooses its own
159
+ failure handling, so a mixed batch of required and optional reads is one round trip
160
+ and every `value` says in its type whether it can be `None`. Reading `value` before
161
+ `run()` raises `RuntimeError`, and a batch runs once. `aggregate` / `try_aggregate`
162
+ stay for lists of one type.
163
+
164
+ ```python
165
+ batch = Multicall3(ctx).batch()
166
+ assets = batch.add(vault.total_assets()) # BatchValue[Amount]
167
+ fuses = batch.add(vault.get_fuses()) # BatchValue[list[FuseAddress]]
168
+ name = batch.try_add(vault.name()) # BatchValue[str | None]
169
+ batch.run()
170
+ assets.value, fuses.value, name.value
171
+ ```
172
+
173
+ `VaultSimulator.observe(call, *, label=None)` returns a `SimulationObservation[T]`
174
+ instead of the simulator, so it no longer chains, and its label is optional (it
175
+ names the read in `result.calls` and in failures). After `run()`,
176
+ `observation.value` is the read decoded as its `Call` decodes it, from the latest
177
+ run; `observation.result` is its `SimulatedCallResult`.
178
+ `CrosschainSimulator.observe(chain_id, call, *, label=None)` returns the same,
179
+ resolved by every relay round. `SimulationResult.get`, `SimulationResult.observations`
180
+ and `SimulatedCallResult.decoded` are removed: a list of results of many types has no
181
+ honest element type, so the typed value lives on the observation. A write queued with
182
+ `add_call` is no longer decoded; decode its return data with the `Call` you queued.
183
+
184
+ ```python
185
+ before = sim.observe(vault.total_assets()) # SimulationObservation[Amount]
186
+ sim.execute([action])
187
+ after = sim.observe(vault.total_assets())
188
+ sim.run()
189
+ after.value - before.value
190
+ ```
191
+
192
+ `Call` is a frozen dataclass and covariant in its result type: assigning to a
193
+ field raises, and a `Call[Amount]` is a `Call[int]` and a `Call[object]`, so a
194
+ `Sequence[Call[object]]` takes any calls without a cast.
141
195
 
142
196
  ### Renamed methods
143
197
 
@@ -721,6 +775,45 @@ string). Calling an MCP tool function
721
775
  directly from Python takes `ChainId`. Their amount, decimals and time fields stay
722
776
  `int`: they are JSON output models, and the SDK values above fit them unchanged.
723
777
 
778
+ ## Typed JSON documents
779
+
780
+ The JSON document builders return `TypedDict`s instead of `dict[str, Any]`; the
781
+ documents themselves (keys, values, order, the MCP schemas) are unchanged, so
782
+ untyped code needs none of this section.
783
+
784
+ | Builder | Returns |
785
+ |---|---|
786
+ | `build_vault_info_json` | `VaultInfoJson` |
787
+ | `build_morpho_blue_market_json` | `MorphoBlueMarketJson` |
788
+ | `build_meta_morpho_json` | `MetaMorphoV1Json \| MetaMorphoV2Json`, told apart by `doc["version"]` |
789
+ | `RoleAccount.to_dict` | `RoleAccountJson` |
790
+
791
+ Every nested block has its own TypedDict (`FeesJson`, `WithdrawManagerJson`,
792
+ `BalanceFuseJson`, `ReconciliationJson`, `HyperCoreJson`, `MorphoSupplyingVaultJson`,
793
+ ...), all importable from `ipor_fusion` and from `ipor_fusion.vault_info` /
794
+ `ipor_fusion.market_info` (`RoleAccountJson` from `ipor_fusion.core`). Values carry the
795
+ SDK's nominal types (`doc["vault"]` is a `PlasmaVaultAddress`, `doc["chain_id"]` a
796
+ `ChainId`). A block that can be `null` is `X | None`: check it before indexing
797
+ (`fees = doc["fees"]; if fees is not None: fees["management_fee_percent"]`). A key
798
+ that only some entries carry is `NotRequired` (a balance fuse's `pct_of_total` and
799
+ `position_breakdown`, the Morpho API keys of the market document): read it with
800
+ `.get`. A decoded substrate entry stays open-ended, `SubstrateEntryJson`
801
+ (`dict[str, str | bool]`), since each decoder adds its own fields.
802
+
803
+ - A variable or parameter annotated `dict[str, Any]` no longer accepts these
804
+ documents (a `TypedDict` is not a `dict[str, Any]`): annotate it with the TypedDict,
805
+ or `Mapping[str, object]` for code that only passes the document on (`json.dumps`
806
+ accepts either).
807
+ - Parsing entry points take `Mapping[str, object]` instead of `Mapping[str, Any]`:
808
+ `decode_external_state_event(log)` and `FusionInstance.from_dict(data)`.
809
+ `VerifierResult.message` is a `Mapping[str, object]`, and `OutboundMessage.raw` is
810
+ `object`: narrow it before use (`isinstance(message.raw, MessageV1)` for CCIP; a
811
+ Stargate `Packet`, or a `(Packet, TaxiMessage)` pair for a token transfer).
812
+ - The `vault info` document's `deployment` is `DeploymentJson` (block, timestamp,
813
+ age) or `DeploymentErrorJson` (`{"error": ...}`) or `None`; `share_price` is
814
+ `SharePriceJson` (`asset`, and `usd` when the asset is priced); `dependency_graph`
815
+ is `DependencyGraphJson` (`edges`, `update_reach`, `update_groups`).
816
+
724
817
  ## Behavior changes
725
818
 
726
819
  These apply to every caller, type-checked or not. Changes described in full above
@@ -775,5 +868,10 @@ are listed with a link.
775
868
  - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
776
869
  documents already were).
777
870
  - `TransactionError.tx_hash` is `0x`-prefixed.
871
+ - An observed simulation read that reverted raises `SimulationError` from its
872
+ `value`, one that returned no data raises `EmptyCallResultError`, and one its
873
+ decoder cannot read raises the decoder's error. 3.x left it out of
874
+ `result.observations` (so `result.get(label)` raised `KeyError`) or reported an
875
+ undecodable read as `None`, indistinguishable from a read that returns `None`.
778
876
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
779
877
  a value that is not an address.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ipor-fusion
3
- Version: 4.0.0.dev151
3
+ Version: 4.0.0.dev156
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
@@ -188,12 +188,12 @@ payload = vault.execute([action]).calldata # raw bytes for an external signer
188
188
 
189
189
  # Simulate first via eth_simulateV1 (no local node); alpha is the account allowed to call execute()
190
190
  sim = VaultSimulator(ctx.web3, vault=vault.address, alpha=Web3.to_checksum_address("0xALPHA"))
191
- sim.observe("before", vault.total_assets())
191
+ before = sim.observe(vault.total_assets()) # SimulationObservation[Amount]
192
192
  sim.execute([action])
193
- sim.observe("after", vault.total_assets())
193
+ after = sim.observe(vault.total_assets())
194
194
  result = sim.run()
195
195
  if result.all_success:
196
- print(result.get("after") - result.get("before"))
196
+ print(after.value - before.value)
197
197
  ```
198
198
 
199
199
  ## CLI Quickstart
@@ -140,12 +140,12 @@ payload = vault.execute([action]).calldata # raw bytes for an external signer
140
140
 
141
141
  # Simulate first via eth_simulateV1 (no local node); alpha is the account allowed to call execute()
142
142
  sim = VaultSimulator(ctx.web3, vault=vault.address, alpha=Web3.to_checksum_address("0xALPHA"))
143
- sim.observe("before", vault.total_assets())
143
+ before = sim.observe(vault.total_assets()) # SimulationObservation[Amount]
144
144
  sim.execute([action])
145
- sim.observe("after", vault.total_assets())
145
+ after = sim.observe(vault.total_assets())
146
146
  result = sim.run()
147
147
  if result.all_success:
148
- print(result.get("after") - result.get("before"))
148
+ print(after.value - before.value)
149
149
  ```
150
150
 
151
151
  ## CLI Quickstart
@@ -396,7 +396,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
396
396
  # - sim.add_call(call=X, from_=OWNER) -> X.send(ctx) by the owner/atomist
397
397
  # (or X.calldata to a multisig / timelock).
398
398
  # - sim.execute([action]) -> plasma_vault.execute([action]).send(ctx) by the alpha.
399
- # - sim.observe("key", X) -> X.call(ctx), a plain read.
399
+ # - read = sim.observe(X), then read.value -> X.call(ctx), a plain read.
400
400
  sim = VaultSimulator(
401
401
  web3=web3, vault=preview.plasma_vault, alpha=ALPHA, block=PINNED_BLOCK
402
402
  )
@@ -467,14 +467,14 @@ def run_simulation(web3: Web3) -> SimulationResult:
467
467
  )
468
468
 
469
469
  # Read the granted substrate set back so we can assert it is EXACTLY our two.
470
- sim.observe("granted_substrates", plasma_vault.get_market_substrates(EULER_MARKET))
470
+ granted_substrates = sim.observe(plasma_vault.get_market_substrates(EULER_MARKET))
471
471
  plan.append("read the granted substrate set back")
472
472
 
473
473
  # Oracle coverage: the vault must be able to price every tracked asset. A fresh
474
474
  # vault's price manager delegates to the global middleware for assets without
475
475
  # an override, so these read the effective prices.
476
- sim.observe("cbeth_price", price_manager.get_asset_price(BASE_CBETH))
477
- sim.observe("weth_price", price_manager.get_asset_price(BASE_WETH))
476
+ cbeth_price_obs = sim.observe(price_manager.get_asset_price(BASE_CBETH))
477
+ weth_price_obs = sim.observe(price_manager.get_asset_price(BASE_WETH))
478
478
  plan.append("verify oracle coverage for cbETH and WETH")
479
479
 
480
480
  # Fund the vault with cbETH collateral. SIMULATION ONLY: an impersonated
@@ -485,7 +485,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
485
485
  call=cbeth.transfer(preview.plasma_vault, COLLATERAL_FUND_AMOUNT),
486
486
  from_=BASE_CBETH_WHALE,
487
487
  )
488
- sim.observe("vault_cbeth_funded", cbeth.balance_of(preview.plasma_vault))
488
+ vault_cbeth_funded = sim.observe(cbeth.balance_of(preview.plasma_vault))
489
489
  plan.append("fund the vault with cbETH collateral")
490
490
 
491
491
  # ── Credit lifecycle, driven by the alpha via PlasmaVault.execute(...). ──
@@ -500,20 +500,22 @@ def run_simulation(web3: Web3) -> SimulationResult:
500
500
 
501
501
  # Borrow WETH against the cbETH collateral; debt is now open.
502
502
  sim.execute([borrow_action(BORROW_AMOUNT)])
503
- sim.observe("debt_after_borrow", _evault_debt_of(EVAULT_WETH, euler_account))
503
+ debt_after_borrow = sim.observe(_evault_debt_of(EVAULT_WETH, euler_account))
504
504
  plan.append("borrow 1 WETH against the cbETH collateral")
505
505
 
506
506
  # Re-value the market while the position is open so the balance fuse's
507
507
  # collateral-minus-debt valuation surfaces into the vault's stored NAV.
508
508
  sim.add_call(call=plasma_vault.update_markets_balances([EULER_MARKET]), from_=ALPHA)
509
- sim.observe("euler_value_open", plasma_vault.total_assets_in_market(EULER_MARKET))
509
+ euler_value_open_obs = sim.observe(
510
+ plasma_vault.total_assets_in_market(EULER_MARKET)
511
+ )
510
512
  plan.append("re-value the open Euler position")
511
513
 
512
514
  # Repay the full debt. MAX_UINT256 caps at the outstanding amount; with no time
513
515
  # advance in this run, interest is negligible and the borrowed WETH covers it.
514
516
  # (Advancing time would accrue interest, requiring a small extra WETH buffer.)
515
517
  sim.execute([repay_action(Amount(MAX_UINT256))])
516
- sim.observe("debt_after_repay", _evault_debt_of(EVAULT_WETH, euler_account))
518
+ debt_after_repay = sim.observe(_evault_debt_of(EVAULT_WETH, euler_account))
517
519
  plan.append("repay the full WETH debt")
518
520
 
519
521
  # Unwind in reverse of setup: disable the WETH controller, then the cbETH
@@ -529,8 +531,10 @@ def run_simulation(web3: Web3) -> SimulationResult:
529
531
  # to fully exit the position, then re-value the market so the exit surfaces.
530
532
  sim.execute([withdraw_collateral_action(Amount(MAX_UINT256))])
531
533
  sim.add_call(call=plasma_vault.update_markets_balances([EULER_MARKET]), from_=ALPHA)
532
- sim.observe("euler_value_closed", plasma_vault.total_assets_in_market(EULER_MARKET))
533
- sim.observe("vault_cbeth_closed", cbeth.balance_of(preview.plasma_vault))
534
+ euler_value_closed_obs = sim.observe(
535
+ plasma_vault.total_assets_in_market(EULER_MARKET)
536
+ )
537
+ vault_cbeth_closed = sim.observe(cbeth.balance_of(preview.plasma_vault))
534
538
  plan.append("withdraw cbETH; the Euler position is fully exited")
535
539
 
536
540
  log.info("transaction plan:")
@@ -544,29 +548,29 @@ def run_simulation(web3: Web3) -> SimulationResult:
544
548
  # is the single eth_simulateV1 call that executed it. From here we read the
545
549
  # recorded observations back and verify the outcome, failing loud on any
546
550
  # mismatch. ─────────────────────────────────────────────────────────────
547
- cbeth_price = result.get("cbeth_price")
548
- weth_price = result.get("weth_price")
551
+ cbeth_price = cbeth_price_obs.value
552
+ weth_price = weth_price_obs.value
549
553
  _check(cbeth_price.amount > 0, "cbETH has no oracle price -- vault cannot value it")
550
554
  _check(weth_price.amount > 0, "WETH has no oracle price -- vault cannot value it")
551
555
 
552
556
  # No unrelated permissions: the granted set is EXACTLY the two substrates we
553
557
  # built, and nothing else. Compare as sets (grant order is not guaranteed).
554
- granted = {bytes(s) for s in result.get("granted_substrates")}
558
+ granted = {bytes(s) for s in granted_substrates.value}
555
559
  _check(
556
560
  granted == {bytes(s) for s in market_substrates()},
557
561
  f"unexpected market substrates granted: {sorted(s.hex() for s in granted)}",
558
562
  )
559
563
 
560
564
  _check(
561
- result.get("vault_cbeth_funded") >= COLLATERAL_FUND_AMOUNT,
565
+ vault_cbeth_funded.value >= COLLATERAL_FUND_AMOUNT,
562
566
  "vault did not receive the cbETH collateral",
563
567
  )
564
568
  # borrow opened debt; repay cleared it -- read straight from the eWETH eVault.
565
- debt_open = result.get("debt_after_borrow")
569
+ debt_open = debt_after_borrow.value
566
570
  _check(debt_open >= BORROW_AMOUNT, f"borrow did not open debt: {debt_open}")
567
571
  _check(
568
- result.get("debt_after_repay") == 0,
569
- f"repay did not clear debt: {result.get('debt_after_repay')}",
572
+ debt_after_repay.value == 0,
573
+ f"repay did not clear debt: {debt_after_repay.value}",
570
574
  )
571
575
  # The balance fuse nets the debt against the collateral. The open position is
572
576
  # worth the supplied cbETH MINUS the WETH debt, in the vault's WETH terms. To
@@ -575,7 +579,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
575
579
  # debt: too narrow a gap means the debt was ignored (a bare "> 0" or "< value"
576
580
  # would miss it, since the collateral valuation rounds down), too wide means it
577
581
  # was double-counted. The band is +/-50% of one debt to absorb rounding.
578
- euler_value_open = result.get("euler_value_open")
582
+ euler_value_open = euler_value_open_obs.value
579
583
  collateral_value = (
580
584
  COLLATERAL_SUPPLY_AMOUNT * cbeth_price.amount // weth_price.amount
581
585
  )
@@ -589,14 +593,14 @@ def run_simulation(web3: Web3) -> SimulationResult:
589
593
  # Fully exited after the withdraw: the Euler market values to dust, and the
590
594
  # supplied cbETH is back on the vault (the ~2 cbETH idle buffer plus the ~10
591
595
  # withdrawn -> roughly the funded amount, minus ERC4626 round-down dust).
592
- euler_value_closed = result.get("euler_value_closed")
596
+ euler_value_closed = euler_value_closed_obs.value
593
597
  _check(
594
598
  euler_value_closed <= EULER_EXIT_DUST,
595
599
  f"Euler position not fully exited: market value {euler_value_closed}",
596
600
  )
597
601
  _check(
598
- result.get("vault_cbeth_closed") >= COLLATERAL_FUND_AMOUNT - EULER_EXIT_DUST,
599
- f"withdrawn cbETH not returned to the vault: {result.get('vault_cbeth_closed')}",
602
+ vault_cbeth_closed.value >= COLLATERAL_FUND_AMOUNT - EULER_EXIT_DUST,
603
+ f"withdrawn cbETH not returned to the vault: {vault_cbeth_closed.value}",
600
604
  )
601
605
 
602
606
  log.info(
@@ -605,7 +609,7 @@ def run_simulation(web3: Web3) -> SimulationResult:
605
609
  cbeth_price.amount,
606
610
  weth_price.amount,
607
611
  debt_open,
608
- result.get("debt_after_repay"),
612
+ debt_after_repay.value,
609
613
  euler_value_open,
610
614
  euler_value_closed,
611
615
  result.gas_used,
@@ -409,10 +409,10 @@ def run_simulation(web3: Web3) -> SimulationResult:
409
409
  )
410
410
  # update_callback_handler exercises FUSE_MANAGER_ROLE, held by OWNER above.
411
411
 
412
- sim.observe("wbtc_price", price_manager.get_asset_price(ARBITRUM_WBTC))
413
- sim.observe("usdc_price", price_manager.get_asset_price(ARBITRUM_USDC))
414
- sim.observe("euler_substrates", vault.get_market_substrates(EULER_MARKET))
415
- sim.observe("swapper_substrates", vault.get_market_substrates(SWAPPER_MARKET))
412
+ wbtc_price = sim.observe(price_manager.get_asset_price(ARBITRUM_WBTC))
413
+ usdc_price = sim.observe(price_manager.get_asset_price(ARBITRUM_USDC))
414
+ euler_substrates = sim.observe(vault.get_market_substrates(EULER_MARKET))
415
+ swapper_substrates = sim.observe(vault.get_market_substrates(SWAPPER_MARKET))
416
416
 
417
417
  # Simulation-only seed. In production WBTC arrives through normal deposits.
418
418
  sim.add_call(
@@ -423,56 +423,54 @@ def run_simulation(web3: Web3) -> SimulationResult:
423
423
  sim.execute([build_loop_action()])
424
424
 
425
425
  sim.add_call(call=vault.update_markets_balances([EULER_MARKET]), from_=ALPHA)
426
- sim.observe(
427
- "collateral_shares",
426
+ collateral_shares = sim.observe(
428
427
  ERC20(EVAULT_WBTC).balance_of(euler_account),
429
428
  )
430
- sim.observe(
431
- "usdc_debt",
429
+ usdc_debt = sim.observe(
432
430
  _debt_of(EVAULT_USDC, euler_account),
433
431
  )
434
- sim.observe("vault_wbtc", wbtc.balance_of(preview.plasma_vault))
435
- sim.observe("vault_usdc", usdc.balance_of(preview.plasma_vault))
436
- sim.observe("euler_value", vault.total_assets_in_market(EULER_MARKET))
437
- sim.observe("total_assets", vault.total_assets())
432
+ vault_wbtc = sim.observe(wbtc.balance_of(preview.plasma_vault))
433
+ vault_usdc = sim.observe(usdc.balance_of(preview.plasma_vault))
434
+ euler_value = sim.observe(vault.total_assets_in_market(EULER_MARKET))
435
+ total_assets = sim.observe(vault.total_assets())
438
436
 
439
437
  result = sim.run()
440
438
  _assert_all_success(result)
441
439
 
442
- _check(result.get("wbtc_price").amount > 0, "WBTC has no oracle price")
443
- _check(result.get("usdc_price").amount > 0, "USDC has no oracle price")
440
+ _check(wbtc_price.value.amount > 0, "WBTC has no oracle price")
441
+ _check(usdc_price.value.amount > 0, "USDC has no oracle price")
444
442
  _check(
445
- {bytes(value) for value in result.get("euler_substrates")}
443
+ {bytes(value) for value in euler_substrates.value}
446
444
  == {bytes(value) for value in euler_market_substrates()},
447
445
  "Euler substrates differ from the narrow WBTC/USDC grant",
448
446
  )
449
447
  _check(
450
- {bytes(value) for value in result.get("swapper_substrates")}
448
+ {bytes(value) for value in swapper_substrates.value}
451
449
  == {bytes(value) for value in swapper_market_substrates()},
452
450
  "swapper substrates differ from the narrow pair/target grant",
453
451
  )
454
- _check(result.get("collateral_shares") > 0, "Euler collateral was not supplied")
452
+ _check(collateral_shares.value > 0, "Euler collateral was not supplied")
455
453
  _check(
456
- result.get("usdc_debt") >= BORROW_USDC,
457
- f"Euler USDC debt was not opened: {result.get('usdc_debt')}",
454
+ usdc_debt.value >= BORROW_USDC,
455
+ f"Euler USDC debt was not opened: {usdc_debt.value}",
458
456
  )
459
- _check(result.get("vault_usdc") == 0, "borrowed USDC was not fully swapped")
457
+ _check(vault_usdc.value == 0, "borrowed USDC was not fully swapped")
460
458
  _check(
461
- result.get("vault_wbtc") < Amount(1_000_000),
462
- f"unexpected WBTC residue after flash repayment: {result.get('vault_wbtc')}",
459
+ vault_wbtc.value < Amount(1_000_000),
460
+ f"unexpected WBTC residue after flash repayment: {vault_wbtc.value}",
463
461
  )
464
- _check(result.get("euler_value") > 0, "Euler net position has no value")
462
+ _check(euler_value.value > 0, "Euler net position has no value")
465
463
  _check(
466
- INITIAL_WBTC * 98 // 100 <= result.get("total_assets") <= INITIAL_WBTC,
467
- f"loop NAV moved outside the 2% safety band: {result.get('total_assets')}",
464
+ INITIAL_WBTC * 98 // 100 <= total_assets.value <= INITIAL_WBTC,
465
+ f"loop NAV moved outside the 2% safety band: {total_assets.value}",
468
466
  )
469
467
 
470
468
  log.info(
471
469
  "OK -- collateral shares=%s, USDC debt=%s, idle WBTC=%s, NAV=%s, gas=%s",
472
- result.get("collateral_shares"),
473
- result.get("usdc_debt"),
474
- result.get("vault_wbtc"),
475
- result.get("total_assets"),
470
+ collateral_shares.value,
471
+ usdc_debt.value,
472
+ vault_wbtc.value,
473
+ total_assets.value,
476
474
  result.gas_used,
477
475
  )
478
476
  return result
@@ -83,6 +83,7 @@ from ipor_fusion import (
83
83
  CrosschainLane,
84
84
  CrosschainSimulator,
85
85
  HyperCoreReader,
86
+ LaneObservation,
86
87
  PlasmaVault,
87
88
  SimulationResult,
88
89
  TimeInForce,
@@ -784,9 +785,14 @@ def redeem_command_preview(web3_arb: Web3, web3_hype: Web3) -> dict[str, int]:
784
785
  }
785
786
 
786
787
 
787
- def _attest(flow: _Tail, tag: str, observation_label: str, *, hub_idle: int = 0) -> int:
788
+ def _attest(
789
+ flow: _Tail,
790
+ tag: str,
791
+ observation: LaneObservation,
792
+ *,
793
+ hub_idle: int = 0,
794
+ ) -> int:
788
795
  lane, hub = flow.lane, flow.hub
789
- observation = flow.simulator.results[HYPEREVM_CHAIN_ID].get(observation_label)
790
796
  proposal = lane.attestation(
791
797
  observation,
792
798
  remote_block=flow.spoke.current_block_number,
@@ -816,18 +822,14 @@ def _attest(flow: _Tail, tag: str, observation_label: str, *, hub_idle: int = 0)
816
822
  from_=ALPHA,
817
823
  label=f"refresh_54_{tag}",
818
824
  )
819
- flow.simulator.observe(
820
- ARBITRUM_CHAIN_ID, f"executor_balance_{tag}", lane.get_balance()
821
- )
822
- flow.simulator.observe(
823
- ARBITRUM_CHAIN_ID,
824
- f"market_total_{tag}",
825
- flow.hub_vault.total_assets_in_market(CROSSCHAIN_MARKET),
825
+ executor_nav = flow.simulator.observe(ARBITRUM_CHAIN_ID, lane.get_balance())
826
+ market_nav = flow.simulator.observe(
827
+ ARBITRUM_CHAIN_ID, flow.hub_vault.total_assets_in_market(CROSSCHAIN_MARKET)
826
828
  )
827
- results = flow.relay()
829
+ flow.relay()
828
830
  attested = int(observation.accounted_balance)
829
- executor_balance = int(results[ARBITRUM_CHAIN_ID].get(f"executor_balance_{tag}"))
830
- market_total = int(results[ARBITRUM_CHAIN_ID].get(f"market_total_{tag}"))
831
+ executor_balance = executor_nav.value
832
+ market_total = market_nav.value
831
833
  _check(
832
834
  executor_balance == attested + hub_idle,
833
835
  f"{tag}: executor NAV {executor_balance} != attested {attested} + idle {hub_idle}",
@@ -908,23 +910,23 @@ def recall_attest_claim(web3_arb: Web3, web3_hype: Web3) -> dict[str, int]:
908
910
  hub.execute([lane.recall(amount=remote_idle, min_return=remote_idle)])
909
911
  flow.advance(BRIDGE_LATENCY)
910
912
  flow.relay()
911
- simulator.observe(ARBITRUM_CHAIN_ID, "idle_after_return", lane.idle_ledger())
912
- simulator.observe(
913
- ARBITRUM_CHAIN_ID, "settled_after_return", lane.settled_remote_balance()
913
+ idle_after_return = simulator.observe(ARBITRUM_CHAIN_ID, lane.idle_ledger())
914
+ settled_after_return = simulator.observe(
915
+ ARBITRUM_CHAIN_ID, lane.settled_remote_balance()
914
916
  )
915
- simulator.observe(
916
- ARBITRUM_CHAIN_ID, "pending_after_return", lane.pending_transfer_count()
917
+ pending_after_return = simulator.observe(
918
+ ARBITRUM_CHAIN_ID, lane.pending_transfer_count()
917
919
  )
918
- simulator.observe(HYPEREVM_CHAIN_ID, "observation_after_return", lane.observation())
919
- results = flow.relay()
920
- idle = int(results[ARBITRUM_CHAIN_ID].get("idle_after_return"))
921
- settled = int(results[ARBITRUM_CHAIN_ID].get("settled_after_return"))
920
+ observation_after_return = simulator.observe(HYPEREVM_CHAIN_ID, lane.observation())
921
+ flow.relay()
922
+ idle = idle_after_return.value
923
+ settled = settled_after_return.value
922
924
  _check(
923
925
  idle == int(remote_idle),
924
926
  f"the hub credited {idle}, the spoke sent {remote_idle} (no token fee on this lane)",
925
927
  )
926
928
  _check(
927
- results[ARBITRUM_CHAIN_ID].get("pending_after_return") == 0,
929
+ pending_after_return.value == 0,
928
930
  "a transfer is still pending",
929
931
  )
930
932
  # The settled bucket keeps exactly the value of the residual shares; the
@@ -937,7 +939,7 @@ def recall_attest_claim(web3_arb: Web3, web3_hype: Web3) -> dict[str, int]:
937
939
  flow.numbers.update({"recalled": idle, "settled_after_return": settled})
938
940
 
939
941
  flow.advance(int(lane.executor.min_update_interval().call(hub_ctx)))
940
- residual = _attest(flow, "residual", "observation_after_return", hub_idle=idle)
942
+ residual = _attest(flow, "residual", observation_after_return.value, hub_idle=idle)
941
943
  _check(
942
944
  residual == settled, f"the residual attestation {residual} != settled {settled}"
943
945
  )
@@ -948,32 +950,31 @@ def recall_attest_claim(web3_arb: Web3, web3_hype: Web3) -> dict[str, int]:
948
950
  from_=ALPHA,
949
951
  label="refresh_54_final",
950
952
  )
951
- simulator.observe(
952
- ARBITRUM_CHAIN_ID, "vault_usdc_after", flow.hub_usdc.balance_of(HUB_VAULT)
953
+ vault_usdc_after = simulator.observe(
954
+ ARBITRUM_CHAIN_ID, flow.hub_usdc.balance_of(HUB_VAULT)
953
955
  )
954
- simulator.observe(ARBITRUM_CHAIN_ID, "idle_after_claim", lane.idle_ledger())
955
- simulator.observe(ARBITRUM_CHAIN_ID, "nav_final", lane.get_balance())
956
- simulator.observe(
956
+ idle_after_claim = simulator.observe(ARBITRUM_CHAIN_ID, lane.idle_ledger())
957
+ nav_final = simulator.observe(ARBITRUM_CHAIN_ID, lane.get_balance())
958
+ market_final = simulator.observe(
957
959
  ARBITRUM_CHAIN_ID,
958
- "market_final",
959
960
  flow.hub_vault.total_assets_in_market(CROSSCHAIN_MARKET),
960
961
  )
961
- results = flow.relay()
962
- vault_after = int(results[ARBITRUM_CHAIN_ID].get("vault_usdc_after"))
962
+ flow.relay()
963
+ vault_after = vault_usdc_after.value
963
964
  _check(
964
965
  vault_after == vault_usdc_before + idle,
965
966
  f"hub vault USDC {vault_after} != {vault_usdc_before} + {idle}",
966
967
  )
967
968
  _check(
968
- results[ARBITRUM_CHAIN_ID].get("idle_after_claim") == 0,
969
+ idle_after_claim.value == 0,
969
970
  "the idle ledger is not empty after the claim",
970
971
  )
971
972
  _check(
972
- int(results[ARBITRUM_CHAIN_ID].get("nav_final")) == residual,
973
+ nav_final.value == residual,
973
974
  "executor NAV != the residual",
974
975
  )
975
976
  _check(
976
- int(results[ARBITRUM_CHAIN_ID].get("market_final")) == residual,
977
+ market_final.value == residual,
977
978
  "market 54 total != the residual",
978
979
  )
979
980
  log.info(