ipor-fusion 4.0.0.dev153__tar.gz → 4.0.0.dev159__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 (303) hide show
  1. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/AGENTS.md +15 -8
  2. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/MIGRATION_4.md +74 -0
  3. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/PKG-INFO +4 -4
  4. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/README.md +3 -3
  5. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/advanced_euler_v2_credit_market_base.py +26 -22
  6. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/advanced_euler_v2_looping_arbitrum.py +27 -29
  7. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/composed_hypercore_flow_arbitrum_hyperevm.py +35 -34
  8. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/crosschain_ccip_usdc_arbitrum_hyperevm.py +87 -92
  9. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/external_state_margin_leg_base.py +170 -96
  10. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/simple_aave_v3_supply_base.py +20 -20
  11. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/pyproject.toml +1 -1
  12. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/__init__.py +92 -1
  13. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/oracle_mapping_view.py +9 -5
  14. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/__init__.py +8 -1
  15. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/contract.py +10 -9
  16. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/external_state_executor.py +18 -11
  17. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/multicall.py +115 -6
  18. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/simulation.py +90 -65
  19. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/lane.py +27 -19
  20. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/simulation.py +13 -6
  21. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/stargate/lane.py +1 -2
  22. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/errors.py +3 -2
  23. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/__init__.py +36 -0
  24. ipor_fusion-4.0.0.dev159/src/ipor_fusion/fuses/events.py +45 -0
  25. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/external_state_events.py +93 -8
  26. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/hypercore_events.py +185 -10
  27. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/mcp/models.py +3 -1
  28. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/__init__.py +26 -0
  29. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/hypercore.py +43 -43
  30. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/hypercore_outcome.py +17 -26
  31. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/lending_health.py +10 -8
  32. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/morpho.py +6 -7
  33. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/oracle_mapping_nodes.py +158 -10
  34. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/oracle_mapping_reader.py +5 -2
  35. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/unpriceable.py +11 -12
  36. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_data.py +74 -94
  37. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_erc20.py +18 -19
  38. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_managers.py +47 -61
  39. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_reads.py +3 -24
  40. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_crosschain_lifecycle.py +114 -110
  41. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_crosschain_recovery.py +155 -92
  42. ipor_fusion-4.0.0.dev159/tests/_event_args.py +29 -0
  43. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_multicall.py +10 -2
  44. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/conftest.py +1 -1
  45. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cache.py +4 -1
  46. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_helpers.py +1 -43
  47. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_oracle_mapping.py +13 -5
  48. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_pending_requests_scan.py +2 -11
  49. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_errors.py +0 -2
  50. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_transport.py +6 -6
  51. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_examples_vaults.py +0 -2
  52. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_external_state_events.py +23 -0
  53. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_external_state_executor.py +23 -18
  54. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_events.py +20 -7
  55. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_live.py +15 -21
  56. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_simulation.py +8 -8
  57. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_mcp_models.py +15 -3
  58. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_multicall.py +158 -0
  59. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_oracle.py +3 -3
  60. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_oracle_mapping.py +59 -43
  61. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_public_api.py +74 -0
  62. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_borrow_aave_v3_mainnet.py +20 -22
  63. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_claim_merkl_wrapper_base.py +12 -14
  64. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_claim_morpho_rewards.py +3 -4
  65. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_crosschain_canary.py +15 -14
  66. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_crosschain_hypercore.py +9 -9
  67. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_crosschain_hyperevm.py +146 -152
  68. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_euler_v2_batch_base.py +5 -6
  69. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_euler_v2_credit_market_base.py +5 -6
  70. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_euler_v2_swap_deploy_base.py +9 -13
  71. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_euler_v2_swap_reconfigure_base.py +5 -6
  72. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_euler_v2_swap_registry_base.py +7 -11
  73. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_external_state_executor_base.py +10 -10
  74. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_looping_aave_v3_base.py +9 -10
  75. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_looping_morpho_blue_base.py +7 -8
  76. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_morpho_blue.py +8 -9
  77. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_pilot_v3.py +34 -38
  78. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_pilot_v4_uniswap.py +30 -35
  79. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_pilot_v5_ramses.py +24 -28
  80. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_pilot_v5_scheduled.py +45 -56
  81. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_release_funds.py +12 -14
  82. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_swap_on_base.py +16 -18
  83. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_vault_from_scratch_base.py +15 -16
  84. ipor_fusion-4.0.0.dev159/tests/test_simulation_observation.py +126 -0
  85. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_unpriceable_tokens.py +9 -0
  86. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_vault_simulator_blocks.py +25 -25
  87. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/typed_client/consumer.py +59 -0
  88. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/typed_client/rejected.py +20 -0
  89. ipor_fusion-4.0.0.dev153/src/ipor_fusion/fuses/events.py +0 -25
  90. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.claude-plugin/marketplace.json +0 -0
  91. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.claude-plugin/plugin.json +0 -0
  92. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.env.example +0 -0
  93. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.gemini/settings.json +0 -0
  94. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.git-blame-ignore-revs +0 -0
  95. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/cd.yml +0 -0
  96. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/ci.yml +0 -0
  97. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/commit-lint.yml +0 -0
  98. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/notify-slack.yml +0 -0
  99. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/python-build.yml +0 -0
  100. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/python-publish.yml +0 -0
  101. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/release.yml +0 -0
  102. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.github/workflows/report-slack.yml +0 -0
  103. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.gitignore +0 -0
  104. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.mcp.json +0 -0
  105. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.pre-commit-config.yaml +0 -0
  106. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/.python-version +0 -0
  107. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/CHANGELOG.md +0 -0
  108. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/CLAUDE.md +0 -0
  109. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/CODEOWNERS +0 -0
  110. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/LICENSE +0 -0
  111. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/build.sh +0 -0
  112. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/CONTRIBUTING.md +0 -0
  113. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/examples/README.md +0 -0
  114. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/pytest.ini +0 -0
  115. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/scripts/commit-check.sh +0 -0
  116. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/scripts/crosschain_readiness.py +0 -0
  117. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/skills/ipor-deploy-vault/SKILL.md +0 -0
  118. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/about.py +0 -0
  119. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cache.py +0 -0
  120. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/chains.py +0 -0
  121. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/__init__.py +0 -0
  122. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/changelog_cmd.py +0 -0
  123. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/config_cmd.py +0 -0
  124. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/config_store.py +0 -0
  125. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/main.py +0 -0
  126. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/market_cmd.py +0 -0
  127. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/options.py +0 -0
  128. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/vault_cmd.py +0 -0
  129. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/vault_health.py +0 -0
  130. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/vault_info_view.py +0 -0
  131. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/vault_markets_view.py +0 -0
  132. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/cli/vault_rendering.py +0 -0
  133. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/config/__init__.py +0 -0
  134. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/config/roles.py +0 -0
  135. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/access.py +0 -0
  136. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/address.py +0 -0
  137. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/context.py +0 -0
  138. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/erc20.py +0 -0
  139. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/fee_manager.py +0 -0
  140. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/fuse.py +0 -0
  141. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/fuse_action.py +0 -0
  142. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/fusion_factory.py +0 -0
  143. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/logs.py +0 -0
  144. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/oracle.py +0 -0
  145. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/plasma_vault.py +0 -0
  146. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/rewards_manager.py +0 -0
  147. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/core/withdraw_manager.py +0 -0
  148. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/__init__.py +0 -0
  149. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/__init__.py +0 -0
  150. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/chainlink.py +0 -0
  151. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/codec.py +0 -0
  152. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/contracts.py +0 -0
  153. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/events.py +0 -0
  154. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/indexer.py +0 -0
  155. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/lane.py +0 -0
  156. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/quote.py +0 -0
  157. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/ccip/transport.py +0 -0
  158. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/contracts.py +0 -0
  159. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/discovery.py +0 -0
  160. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/errors.py +0 -0
  161. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/fuses/__init__.py +0 -0
  162. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/fuses/base.py +0 -0
  163. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/fuses/ccip.py +0 -0
  164. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/fuses/stargate.py +0 -0
  165. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/logs.py +0 -0
  166. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/messages.py +0 -0
  167. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/stargate/__init__.py +0 -0
  168. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/stargate/contracts.py +0 -0
  169. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/stargate/layerzero.py +0 -0
  170. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/stargate/transport.py +0 -0
  171. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/crosschain/transport.py +0 -0
  172. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/dependency_graph.py +0 -0
  173. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/entry_cli.py +0 -0
  174. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/entry_mcp.py +0 -0
  175. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/explorer.py +0 -0
  176. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/field_docs.py +0 -0
  177. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/aave_v3.py +0 -0
  178. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/async_action.py +0 -0
  179. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/base.py +0 -0
  180. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/compound_v3.py +0 -0
  181. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/erc4626.py +0 -0
  182. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/euler_v2.py +0 -0
  183. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/external_state.py +0 -0
  184. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/fluid_instadapp.py +0 -0
  185. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/gearbox_v3.py +0 -0
  186. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/hypercore.py +0 -0
  187. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/merkl.py +0 -0
  188. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/morpho.py +0 -0
  189. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/ramses_v2.py +0 -0
  190. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/uniswap_v3.py +0 -0
  191. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/fuses/universal.py +0 -0
  192. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/guide/__init__.py +0 -0
  193. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/guide/architecture.md +0 -0
  194. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/guide/glossary.md +0 -0
  195. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/guide/invariants.md +0 -0
  196. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/guide/quickstart.md +0 -0
  197. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/market_ids.py +0 -0
  198. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/market_info.py +0 -0
  199. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/mcp/__init__.py +0 -0
  200. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/mcp/guide.py +0 -0
  201. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/mcp/server.py +0 -0
  202. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/morpho_api.py +0 -0
  203. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/py.typed +0 -0
  204. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/aave_v3.py +0 -0
  205. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/aave_v4.py +0 -0
  206. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/compound_v3.py +0 -0
  207. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/oracle_mapping.py +0 -0
  208. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/position_manager.py +0 -0
  209. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/ramses_v2.py +0 -0
  210. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/readers/uniswap_v3.py +0 -0
  211. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/substrates.py +0 -0
  212. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/types.py +0 -0
  213. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/validation.py +0 -0
  214. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_info.py +0 -0
  215. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/src/ipor_fusion/vault_positions.py +0 -0
  216. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/UniswapV3UniversalRouter.py +0 -0
  217. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_crosschain.py +0 -0
  218. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_euler_v2.py +0 -0
  219. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_foundry.py +0 -0
  220. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_guide.py +0 -0
  221. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_hypercore_model.py +0 -0
  222. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_hypercore_shadow.py +0 -0
  223. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_simulate.py +0 -0
  224. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/_validation.py +0 -0
  225. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/addresses.py +0 -0
  226. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/constants.py +0 -0
  227. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/hypercore_hip3_flow.json +0 -0
  228. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/hypercore_market55.json +0 -0
  229. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/vault_info_golden/clean.json +0 -0
  230. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/vault_info_golden/clean.txt +0 -0
  231. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/vault_info_golden/rich.json +0 -0
  232. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/fixtures/vault_info_golden/rich.txt +0 -0
  233. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_about.py +0 -0
  234. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_access_manager.py +0 -0
  235. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_chains.py +0 -0
  236. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_changelog.py +0 -0
  237. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_commands.py +0 -0
  238. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_config_store.py +0 -0
  239. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_hypercore.py +0 -0
  240. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_lending_health.py +0 -0
  241. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_market_morpho_blue.py +0 -0
  242. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_role_accounts.py +0 -0
  243. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_vault_info_golden.py +0 -0
  244. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_vault_predates_getters.py +0 -0
  245. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_cli_vault_validate.py +0 -0
  246. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_contract_call.py +0 -0
  247. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_contract_encoder.py +0 -0
  248. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_ccip_events.py +0 -0
  249. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_ccip_indexer.py +0 -0
  250. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_ccip_quote.py +0 -0
  251. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_chainlink.py +0 -0
  252. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_contracts.py +0 -0
  253. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_encoding.py +0 -0
  254. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_lane.py +0 -0
  255. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_layering.py +0 -0
  256. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_readiness.py +0 -0
  257. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_crosschain_readiness_report.py +0 -0
  258. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_docs_typing.py +0 -0
  259. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_erc20.py +0 -0
  260. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_error_hierarchy.py +0 -0
  261. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_examples_composed_hypercore.py +0 -0
  262. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_examples_crosschain.py +0 -0
  263. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_examples_euler_looping_arbitrum.py +0 -0
  264. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_explorer.py +0 -0
  265. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_fee_manager.py +0 -0
  266. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_fuse_encoding.py +0 -0
  267. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_fusion_instance.py +0 -0
  268. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_guide.py +0 -0
  269. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_deployment.py +0 -0
  270. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_flow.py +0 -0
  271. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_outcome.py +0 -0
  272. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_hypercore_readers.py +0 -0
  273. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_logs.py +0 -0
  274. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_mcp_hypercore.py +0 -0
  275. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_mcp_server.py +0 -0
  276. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_mcp_vault_validate.py +0 -0
  277. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_plasma_vault.py +0 -0
  278. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_public_type_reachability.py +0 -0
  279. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_reader_encoding.py +0 -0
  280. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_readiness_selection.py +0 -0
  281. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_revert_decoding.py +0 -0
  282. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_rewards_manager.py +0 -0
  283. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_roles.py +0 -0
  284. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_sdk_layering.py +0 -0
  285. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_crosschain_lifecycle.py +0 -0
  286. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_examples_arbitrum.py +0 -0
  287. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_examples_base.py +0 -0
  288. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_examples_crosschain.py +0 -0
  289. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_fusion_factory_clone_base.py +0 -0
  290. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_plasma_vault_setters_base.py +0 -0
  291. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_simulate_readers_integration.py +0 -0
  292. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_solidity_compiler.py +0 -0
  293. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_solidity_mirrors.py +0 -0
  294. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_substrates.py +0 -0
  295. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_types_and_events.py +0 -0
  296. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_validation.py +0 -0
  297. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_vault_data_block.py +0 -0
  298. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_vault_dep_graph.py +0 -0
  299. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_vault_info_onchain_base.py +0 -0
  300. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_vault_info_onchain_base_no_withdraw_manager.py +0 -0
  301. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_web3_context.py +0 -0
  302. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/tests/test_withdraw_manager.py +0 -0
  303. {ipor_fusion-4.0.0.dev153 → ipor_fusion-4.0.0.dev159}/uv.lock +0 -0
@@ -207,8 +207,8 @@ 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`
@@ -340,10 +340,14 @@ or examples.
340
340
  never consults the executing context.
341
341
  - Batch independent reads: `Multicall3(ctx).aggregate(calls)` (raises as
342
342
  `.call(ctx)` would) or `.try_aggregate(calls)` (``None`` per failed read) runs
343
- many `Call`s in one `eth_call` at one block; every batch, re-run and
344
- no-Multicall fallback runs on its `ctx`. A loop of `.call(ctx)`s, or a
345
- thread pool of them, costs one round trip each; RPC latency, not decoding,
346
- 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.
347
351
  - Fuses (`fuses/`) are stateless encoders: a method returns an immutable
348
352
  `FuseAction(fuse_address, calldata)` and touches no chain. `Fuse` builds it
349
353
  via `self._action_raw(solidity_signature, values)`; the shared `_validate_*`
@@ -354,8 +358,11 @@ or examples.
354
358
  pins one read.
355
359
  - Simulate before sending: `VaultSimulator(web3, vault=, alpha=)` batches
356
360
  `execute` and `observe` calls into one `eth_simulateV1` round trip, across
357
- blocks via `next_block`. Integration tests and examples use it instead of a
358
- 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.
359
366
  - Amounts are raw on-chain integers; `types.py` `NewType`s (`Amount`, `Shares`,
360
367
  `MarketId`, ...) name the unit, and nothing scales by decimals.
361
368
  - Three address roles are nominal: `PlasmaVaultAddress`, `FuseAddress` and
@@ -142,6 +142,56 @@ placeholder wrapper.
142
142
  | `executor.parse_balance_proposed(log)` / `find_balance_proposed(logs)` | `...(log, chain_id=...)` / `...(logs, chain_id=...)` |
143
143
  | `CcipLane(executor=, dispatcher=, spoke_chain_id=, ...)` | `CcipLane(hub_ctx=, spoke_ctx=, executor=, dispatcher=, ...)` (same for `StargateLane`) |
144
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.
145
195
 
146
196
  ### Renamed methods
147
197
 
@@ -737,6 +787,7 @@ untyped code needs none of this section.
737
787
  | `build_morpho_blue_market_json` | `MorphoBlueMarketJson` |
738
788
  | `build_meta_morpho_json` | `MetaMorphoV1Json \| MetaMorphoV2Json`, told apart by `doc["version"]` |
739
789
  | `RoleAccount.to_dict` | `RoleAccountJson` |
790
+ | `OracleMapping.to_dict` / `OracleNode.to_dict` | `OracleMappingJson` / `OracleNodeJson` |
740
791
 
741
792
  Every nested block has its own TypedDict (`FeesJson`, `WithdrawManagerJson`,
742
793
  `BalanceFuseJson`, `ReconciliationJson`, `HyperCoreJson`, `MorphoSupplyingVaultJson`,
@@ -763,6 +814,24 @@ that only some entries carry is `NotRequired` (a balance fuse's `pct_of_total` a
763
814
  age) or `DeploymentErrorJson` (`{"error": ...}`) or `None`; `share_price` is
764
815
  `SharePriceJson` (`asset`, and `usd` when the asset is priced); `dependency_graph`
765
816
  is `DependencyGraphJson` (`edges`, `update_reach`, `update_groups`).
817
+ - `OracleNode.source_detail` is `SourceDetailJson`, one TypedDict per feed kind
818
+ (`ChainlinkDetailJson`, `DualXrefDetailJson`, `Erc4626DetailJson`,
819
+ `MorphoFeedDetailJson`, `MiddlewareFallbackDetailJson`; importable from
820
+ `ipor_fusion` and `ipor_fusion.readers`). Narrow by a key before reading a
821
+ kind-specific one: `if detail is not None and "aggregator" in detail:
822
+ detail["description"]`.
823
+ - Event `args` are typed per event kind: `ExternalStateEvent.args` is
824
+ `ExternalStateEventArgs` and `HyperCoreEvent.args` is `HyperCoreEventArgs`, a union
825
+ of one TypedDict per event (`ExternalStateUnpausedArgs`,
826
+ `HyperCoreDepositFuseEnterArgs`, ...; `EXTERNAL_STATE_EVENT_ARGS` /
827
+ `HYPERCORE_EVENT_ARGS` map an event name to its shape). Values carry nominal types
828
+ (`Amount`, `MarketId`, `FuseAddress`, `HyperCoreTokenIndex`, `PayloadHash`, web3's
829
+ `Timestamp`, ...). `event.args["signer"]` no longer type-checks on its own: narrow
830
+ with a key test (`if "signer" in event.args:`). `HyperCoreEvent.fuse` is a
831
+ `FuseAddress | None`.
832
+ - `RawLog` (the log shape `ExternalStateExecutor.parse_balance_proposed` /
833
+ `find_balance_proposed` accept) is `Mapping[str, object]`, not `Mapping[str, Any]`;
834
+ a web3 `LogReceipt` still fits it.
766
835
 
767
836
  ## Behavior changes
768
837
 
@@ -818,5 +887,10 @@ are listed with a link.
818
887
  - `LendingMarketHealth.substrate_id` for a Morpho market is `0x`-prefixed (the JSON
819
888
  documents already were).
820
889
  - `TransactionError.tx_hash` is `0x`-prefixed.
890
+ - An observed simulation read that reverted raises `SimulationError` from its
891
+ `value`, one that returned no data raises `EmptyCallResultError`, and one its
892
+ decoder cannot read raises the decoder's error. 3.x left it out of
893
+ `result.observations` (so `result.get(label)` raised `KeyError`) or reported an
894
+ undecodable read as `None`, indistinguishable from a read that returns `None`.
821
895
  - Saved config entries are checksummed when loaded; the MCP `vault_add` tool rejects
822
896
  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.dev153
3
+ Version: 4.0.0.dev159
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(