@humanode-hyperlane/rebalancer 27.2.2 → 1029.1.1

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 (295) hide show
  1. package/dist/bridges/LiFiBridge.d.ts +3 -3
  2. package/dist/bridges/LiFiBridge.d.ts.map +1 -1
  3. package/dist/bridges/LiFiBridge.js +66 -55
  4. package/dist/bridges/LiFiBridge.js.map +1 -1
  5. package/dist/bridges/LiFiBridge.test.js +213 -0
  6. package/dist/bridges/LiFiBridge.test.js.map +1 -1
  7. package/dist/config/RebalancerConfig.d.ts +4 -3
  8. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  9. package/dist/config/RebalancerConfig.js +7 -5
  10. package/dist/config/RebalancerConfig.js.map +1 -1
  11. package/dist/config/RebalancerConfig.test.js +278 -2
  12. package/dist/config/RebalancerConfig.test.js.map +1 -1
  13. package/dist/config/types.d.ts +707 -2664
  14. package/dist/config/types.d.ts.map +1 -1
  15. package/dist/config/types.js +62 -7
  16. package/dist/config/types.js.map +1 -1
  17. package/dist/core/InventoryRebalancer.d.ts +13 -19
  18. package/dist/core/InventoryRebalancer.d.ts.map +1 -1
  19. package/dist/core/InventoryRebalancer.js +404 -276
  20. package/dist/core/InventoryRebalancer.js.map +1 -1
  21. package/dist/core/InventoryRebalancer.test.js +814 -25
  22. package/dist/core/InventoryRebalancer.test.js.map +1 -1
  23. package/dist/core/Rebalancer.d.ts.map +1 -1
  24. package/dist/core/Rebalancer.js +12 -6
  25. package/dist/core/Rebalancer.js.map +1 -1
  26. package/dist/core/Rebalancer.test.js +51 -0
  27. package/dist/core/Rebalancer.test.js.map +1 -1
  28. package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
  29. package/dist/core/RebalancerOrchestrator.test.js +0 -1
  30. package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
  31. package/dist/core/RebalancerService.d.ts.map +1 -1
  32. package/dist/core/RebalancerService.js +3 -2
  33. package/dist/core/RebalancerService.js.map +1 -1
  34. package/dist/core/RebalancerService.test.js +24 -0
  35. package/dist/core/RebalancerService.test.js.map +1 -1
  36. package/dist/e2e/fixtures/routes.d.ts +3 -3
  37. package/dist/e2e/fixtures/routes.d.ts.map +1 -1
  38. package/dist/e2e/harness/BaseLocalDeploymentManager.d.ts.map +1 -1
  39. package/dist/e2e/harness/BaseLocalDeploymentManager.js +3 -2
  40. package/dist/e2e/harness/BaseLocalDeploymentManager.js.map +1 -1
  41. package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.d.ts.map +1 -1
  42. package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.js.map +1 -1
  43. package/dist/e2e/harness/Erc20LocalDeploymentManager.d.ts.map +1 -1
  44. package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
  45. package/dist/e2e/harness/ForkIndexer.js.map +1 -1
  46. package/dist/e2e/harness/LocalProvider.d.ts +3 -0
  47. package/dist/e2e/harness/LocalProvider.d.ts.map +1 -0
  48. package/dist/e2e/harness/LocalProvider.js +9 -0
  49. package/dist/e2e/harness/LocalProvider.js.map +1 -0
  50. package/dist/e2e/harness/LocalProvider.test.d.ts +2 -0
  51. package/dist/e2e/harness/LocalProvider.test.d.ts.map +1 -0
  52. package/dist/e2e/harness/LocalProvider.test.js +14 -0
  53. package/dist/e2e/harness/LocalProvider.test.js.map +1 -0
  54. package/dist/e2e/harness/MixedLocalDeploymentManager.d.ts.map +1 -1
  55. package/dist/e2e/harness/MixedLocalDeploymentManager.js.map +1 -1
  56. package/dist/e2e/harness/MockExplorerClient.d.ts.map +1 -1
  57. package/dist/e2e/harness/MockExplorerClient.js.map +1 -1
  58. package/dist/e2e/harness/MockExternalBridge.d.ts.map +1 -1
  59. package/dist/e2e/harness/MockExternalBridge.js.map +1 -1
  60. package/dist/e2e/harness/NativeLocalDeploymentManager.d.ts.map +1 -1
  61. package/dist/e2e/harness/NativeLocalDeploymentManager.js +2 -1
  62. package/dist/e2e/harness/NativeLocalDeploymentManager.js.map +1 -1
  63. package/dist/e2e/harness/SnapshotHelper.d.ts.map +1 -1
  64. package/dist/e2e/harness/SnapshotHelper.js +3 -5
  65. package/dist/e2e/harness/SnapshotHelper.js.map +1 -1
  66. package/dist/e2e/harness/TestHelpers.js +1 -2
  67. package/dist/e2e/harness/TestHelpers.js.map +1 -1
  68. package/dist/e2e/harness/TestRebalancer.d.ts +5 -1
  69. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  70. package/dist/e2e/harness/TestRebalancer.js +14 -4
  71. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  72. package/dist/e2e/relayer-denylist.e2e-test.d.ts +2 -0
  73. package/dist/e2e/relayer-denylist.e2e-test.d.ts.map +1 -0
  74. package/dist/e2e/relayer-denylist.e2e-test.js +388 -0
  75. package/dist/e2e/relayer-denylist.e2e-test.js.map +1 -0
  76. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  77. package/dist/factories/RebalancerContextFactory.js +61 -31
  78. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  79. package/dist/factories/RebalancerContextFactory.test.js +336 -5
  80. package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
  81. package/dist/index.d.ts +2 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/interfaces/IRebalancer.d.ts +4 -2
  86. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  87. package/dist/metrics/Metrics.d.ts.map +1 -1
  88. package/dist/metrics/Metrics.js +1 -1
  89. package/dist/metrics/Metrics.js.map +1 -1
  90. package/dist/metrics/PriceGetter.d.ts +1 -0
  91. package/dist/metrics/PriceGetter.d.ts.map +1 -1
  92. package/dist/metrics/PriceGetter.js +14 -4
  93. package/dist/metrics/PriceGetter.js.map +1 -1
  94. package/dist/metrics/PriceGetter.test.d.ts +2 -0
  95. package/dist/metrics/PriceGetter.test.d.ts.map +1 -0
  96. package/dist/metrics/PriceGetter.test.js +84 -0
  97. package/dist/metrics/PriceGetter.test.js.map +1 -0
  98. package/dist/metrics/scripts/metrics.d.ts +4 -4
  99. package/dist/monitor/Monitor.d.ts +1 -0
  100. package/dist/monitor/Monitor.d.ts.map +1 -1
  101. package/dist/monitor/Monitor.js +55 -46
  102. package/dist/monitor/Monitor.js.map +1 -1
  103. package/dist/monitor/Monitor.test.d.ts +2 -0
  104. package/dist/monitor/Monitor.test.d.ts.map +1 -0
  105. package/dist/monitor/Monitor.test.js +346 -0
  106. package/dist/monitor/Monitor.test.js.map +1 -0
  107. package/dist/service.d.ts +1 -1
  108. package/dist/service.d.ts.map +1 -1
  109. package/dist/service.js +9 -5
  110. package/dist/service.js.map +1 -1
  111. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  112. package/dist/strategy/BaseStrategy.js +13 -11
  113. package/dist/strategy/BaseStrategy.js.map +1 -1
  114. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
  115. package/dist/strategy/CollateralDeficitStrategy.js +2 -2
  116. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
  117. package/dist/strategy/CompositeStrategy.d.ts.map +1 -1
  118. package/dist/strategy/CompositeStrategy.js.map +1 -1
  119. package/dist/strategy/CompositeStrategy.test.js.map +1 -1
  120. package/dist/strategy/MinAmountStrategy.d.ts +1 -0
  121. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
  122. package/dist/strategy/MinAmountStrategy.js +12 -8
  123. package/dist/strategy/MinAmountStrategy.js.map +1 -1
  124. package/dist/strategy/MinAmountStrategy.test.js +189 -2
  125. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  126. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  127. package/dist/strategy/StrategyFactory.js.map +1 -1
  128. package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
  129. package/dist/test/helpers.d.ts +11 -3
  130. package/dist/test/helpers.d.ts.map +1 -1
  131. package/dist/test/helpers.js +9 -11
  132. package/dist/test/helpers.js.map +1 -1
  133. package/dist/test/lifiMocks.d.ts +6 -6
  134. package/dist/test/lifiMocks.d.ts.map +1 -1
  135. package/dist/test/lifiMocks.js +5 -2
  136. package/dist/test/lifiMocks.js.map +1 -1
  137. package/dist/test/pendingTransferMiddlewareMocks.d.ts +17 -0
  138. package/dist/test/pendingTransferMiddlewareMocks.d.ts.map +1 -0
  139. package/dist/test/pendingTransferMiddlewareMocks.js +43 -0
  140. package/dist/test/pendingTransferMiddlewareMocks.js.map +1 -0
  141. package/dist/tracking/ActionTracker.d.ts +4 -0
  142. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  143. package/dist/tracking/ActionTracker.js +76 -9
  144. package/dist/tracking/ActionTracker.js.map +1 -1
  145. package/dist/tracking/ActionTracker.test.js +418 -1
  146. package/dist/tracking/ActionTracker.test.js.map +1 -1
  147. package/dist/tracking/IActionTracker.d.ts +4 -2
  148. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  149. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
  150. package/dist/tracking/InflightContextAdapter.js.map +1 -1
  151. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts +64 -0
  152. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts.map +1 -0
  153. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js +474 -0
  154. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js.map +1 -0
  155. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts +2 -0
  156. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts.map +1 -0
  157. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js +1168 -0
  158. package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js.map +1 -0
  159. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts +61 -0
  160. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts.map +1 -0
  161. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js +160 -0
  162. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js.map +1 -0
  163. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts +2 -0
  164. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts.map +1 -0
  165. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js +369 -0
  166. package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js.map +1 -0
  167. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts +60 -0
  168. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts.map +1 -0
  169. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js +288 -0
  170. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js.map +1 -0
  171. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts +2 -0
  172. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts.map +1 -0
  173. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js +606 -0
  174. package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js.map +1 -0
  175. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts +25 -0
  176. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts.map +1 -0
  177. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js +41 -0
  178. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js.map +1 -0
  179. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts +2 -0
  180. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts.map +1 -0
  181. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js +141 -0
  182. package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js.map +1 -0
  183. package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts +12 -0
  184. package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts.map +1 -0
  185. package/dist/tracking/pendingTransferMiddlewares/matchBody.js +29 -0
  186. package/dist/tracking/pendingTransferMiddlewares/matchBody.js.map +1 -0
  187. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts +59 -0
  188. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts.map +1 -0
  189. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js +661 -0
  190. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js.map +1 -0
  191. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts +2 -0
  192. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts.map +1 -0
  193. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js +1093 -0
  194. package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js.map +1 -0
  195. package/dist/tracking/pendingTransferMiddlewares/types.d.ts +2 -0
  196. package/dist/tracking/pendingTransferMiddlewares/types.d.ts.map +1 -0
  197. package/dist/tracking/pendingTransferMiddlewares/types.js +4 -0
  198. package/dist/tracking/pendingTransferMiddlewares/types.js.map +1 -0
  199. package/dist/tracking/store/InMemoryStore.d.ts.map +1 -1
  200. package/dist/utils/ExplorerClient.d.ts +1 -0
  201. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  202. package/dist/utils/ExplorerClient.js +8 -3
  203. package/dist/utils/ExplorerClient.js.map +1 -1
  204. package/dist/utils/ExplorerClient.test.js +25 -0
  205. package/dist/utils/ExplorerClient.test.js.map +1 -1
  206. package/dist/utils/balanceUtils.d.ts +7 -1
  207. package/dist/utils/balanceUtils.d.ts.map +1 -1
  208. package/dist/utils/balanceUtils.js +39 -1
  209. package/dist/utils/balanceUtils.js.map +1 -1
  210. package/dist/utils/balanceUtils.test.js +55 -1
  211. package/dist/utils/balanceUtils.test.js.map +1 -1
  212. package/dist/utils/blockTag.d.ts +5 -4
  213. package/dist/utils/blockTag.d.ts.map +1 -1
  214. package/dist/utils/blockTag.js +26 -10
  215. package/dist/utils/blockTag.js.map +1 -1
  216. package/dist/utils/blockTag.test.d.ts +2 -0
  217. package/dist/utils/blockTag.test.d.ts.map +1 -0
  218. package/dist/utils/blockTag.test.js +132 -0
  219. package/dist/utils/blockTag.test.js.map +1 -0
  220. package/dist/utils/bridgeUtils.js.map +1 -1
  221. package/dist/utils/errors.d.ts.map +1 -1
  222. package/dist/utils/gasEstimation.d.ts +2 -1
  223. package/dist/utils/gasEstimation.d.ts.map +1 -1
  224. package/dist/utils/gasEstimation.js +16 -13
  225. package/dist/utils/gasEstimation.js.map +1 -1
  226. package/dist/utils/gasEstimation.test.d.ts +2 -0
  227. package/dist/utils/gasEstimation.test.d.ts.map +1 -0
  228. package/dist/utils/gasEstimation.test.js +104 -0
  229. package/dist/utils/gasEstimation.test.js.map +1 -0
  230. package/dist/utils/tokenUtils.d.ts.map +1 -1
  231. package/dist/utils/tokenUtils.js +5 -2
  232. package/dist/utils/tokenUtils.js.map +1 -1
  233. package/package.json +23 -22
  234. package/src/bridges/LiFiBridge.test.ts +270 -0
  235. package/src/bridges/LiFiBridge.ts +90 -71
  236. package/src/config/RebalancerConfig.test.ts +315 -1
  237. package/src/config/RebalancerConfig.ts +6 -3
  238. package/src/config/types.ts +98 -8
  239. package/src/core/InventoryRebalancer.test.ts +1092 -38
  240. package/src/core/InventoryRebalancer.ts +584 -351
  241. package/src/core/Rebalancer.test.ts +84 -0
  242. package/src/core/Rebalancer.ts +22 -6
  243. package/src/core/RebalancerOrchestrator.test.ts +0 -1
  244. package/src/core/RebalancerService.test.ts +35 -0
  245. package/src/core/RebalancerService.ts +3 -2
  246. package/src/e2e/harness/BaseLocalDeploymentManager.ts +3 -1
  247. package/src/e2e/harness/LocalProvider.test.ts +16 -0
  248. package/src/e2e/harness/LocalProvider.ts +10 -0
  249. package/src/e2e/harness/NativeLocalDeploymentManager.ts +2 -1
  250. package/src/e2e/harness/SnapshotHelper.ts +2 -5
  251. package/src/e2e/harness/TestHelpers.ts +3 -3
  252. package/src/e2e/harness/TestRebalancer.ts +21 -1
  253. package/src/e2e/relayer-denylist.e2e-test.ts +558 -0
  254. package/src/factories/RebalancerContextFactory.test.ts +489 -8
  255. package/src/factories/RebalancerContextFactory.ts +86 -36
  256. package/src/index.ts +2 -0
  257. package/src/interfaces/IRebalancer.ts +4 -1
  258. package/src/metrics/Metrics.ts +1 -0
  259. package/src/metrics/PriceGetter.test.ts +113 -0
  260. package/src/metrics/PriceGetter.ts +18 -3
  261. package/src/monitor/Monitor.test.ts +408 -0
  262. package/src/monitor/Monitor.ts +123 -97
  263. package/src/service.ts +14 -8
  264. package/src/strategy/BaseStrategy.ts +18 -15
  265. package/src/strategy/CollateralDeficitStrategy.ts +4 -3
  266. package/src/strategy/MinAmountStrategy.test.ts +238 -2
  267. package/src/strategy/MinAmountStrategy.ts +29 -17
  268. package/src/test/helpers.ts +13 -12
  269. package/src/test/lifiMocks.ts +5 -2
  270. package/src/test/pendingTransferMiddlewareMocks.ts +65 -0
  271. package/src/tracking/ActionTracker.test.ts +635 -3
  272. package/src/tracking/ActionTracker.ts +92 -12
  273. package/src/tracking/IActionTracker.ts +4 -2
  274. package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.ts +1693 -0
  275. package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.ts +784 -0
  276. package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.ts +554 -0
  277. package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.ts +233 -0
  278. package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.ts +894 -0
  279. package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.ts +404 -0
  280. package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.ts +205 -0
  281. package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.ts +89 -0
  282. package/src/tracking/pendingTransferMiddlewares/fixtures/mainnet-denylist.json +3556 -0
  283. package/src/tracking/pendingTransferMiddlewares/matchBody.ts +43 -0
  284. package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.ts +1220 -0
  285. package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.ts +934 -0
  286. package/src/tracking/pendingTransferMiddlewares/types.ts +3 -0
  287. package/src/utils/ExplorerClient.test.ts +42 -0
  288. package/src/utils/ExplorerClient.ts +14 -3
  289. package/src/utils/balanceUtils.test.ts +87 -1
  290. package/src/utils/balanceUtils.ts +73 -2
  291. package/src/utils/blockTag.test.ts +173 -0
  292. package/src/utils/blockTag.ts +35 -15
  293. package/src/utils/gasEstimation.test.ts +192 -0
  294. package/src/utils/gasEstimation.ts +17 -11
  295. package/src/utils/tokenUtils.ts +5 -2
@@ -3,14 +3,12 @@ import type { Logger } from 'pino';
3
3
  import {
4
4
  type ChainName,
5
5
  HyperlaneCore,
6
- type InterchainGasQuote,
7
6
  type MultiProtocolSignerSignerAccountInfo,
8
7
  type MultiProvider,
9
- Token,
10
8
  ProviderType,
11
9
  SealevelCoreAdapter,
12
10
  TOKEN_COLLATERALIZED_STANDARDS,
13
- TokenAmount,
11
+ type Token,
14
12
  type WarpTypedTransaction,
15
13
  type WarpCore,
16
14
  WarpTxCategory,
@@ -21,7 +19,8 @@ import {
21
19
  ProtocolType,
22
20
  assert,
23
21
  ensure0x,
24
- isZeroishAddress,
22
+ fromWei,
23
+ isEVMLike,
25
24
  } from '@humanode-hyperlane/utils';
26
25
 
27
26
  import type { ExternalBridgeType } from '../config/types.js';
@@ -50,6 +49,11 @@ import {
50
49
  } from '../utils/tokenUtils.js';
51
50
  import { parseSolanaPrivateKey } from '../utils/solanaKeyParser.js';
52
51
  import { toProtocolTransaction } from '../utils/transactionUtils.js';
52
+ import {
53
+ alignLocalToCanonical,
54
+ denormalizeToLocal,
55
+ normalizeToCanonical,
56
+ } from '../utils/balanceUtils.js';
53
57
 
54
58
  /**
55
59
  * Buffer percentage to add when bridging inventory.
@@ -71,6 +75,86 @@ const GAS_COST_MULTIPLIER = 20n;
71
75
  */
72
76
  const MAX_GAS_PERCENT_THRESHOLD = 10n;
73
77
 
78
+ type BridgeCapacity = {
79
+ maxSourceInput: bigint;
80
+ maxTargetOutput: bigint;
81
+ };
82
+
83
+ type BridgeQuoteMode = 'forward' | 'reverse';
84
+
85
+ type InventoryMovementExecutionResult =
86
+ | {
87
+ success: true;
88
+ txHash: string;
89
+ inputRequired: bigint;
90
+ quotedOutput: bigint;
91
+ quotedOutputMin: bigint;
92
+ quoteModeUsed: BridgeQuoteMode;
93
+ }
94
+ | {
95
+ success: false;
96
+ error: string;
97
+ };
98
+ const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
99
+ 'balance may be insufficient',
100
+ 'transfer amount exceeds balance',
101
+ 'insufficient balance',
102
+ ];
103
+
104
+ function hasRecoverableMaxTransferErrorMessage(message: string): boolean {
105
+ const normalized = message.toLowerCase();
106
+ return (
107
+ normalized.includes('unpredictable_gas_limit') ||
108
+ RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES.some((pattern) =>
109
+ normalized.includes(pattern),
110
+ )
111
+ );
112
+ }
113
+
114
+ function isRecoverableMaxTransferProbeError(error: unknown): boolean {
115
+ const seen = new Set<unknown>();
116
+ const stack: unknown[] = [error];
117
+
118
+ while (stack.length > 0) {
119
+ const current = stack.pop();
120
+ if (current == null) continue;
121
+
122
+ if (typeof current === 'string') {
123
+ if (hasRecoverableMaxTransferErrorMessage(current)) return true;
124
+ continue;
125
+ }
126
+
127
+ if (typeof current !== 'object') continue;
128
+ if (seen.has(current)) continue;
129
+ seen.add(current);
130
+
131
+ const candidate = current as {
132
+ code?: unknown;
133
+ message?: unknown;
134
+ cause?: unknown;
135
+ error?: unknown;
136
+ };
137
+
138
+ if (
139
+ typeof candidate.code === 'string' &&
140
+ candidate.code.toUpperCase() === 'UNPREDICTABLE_GAS_LIMIT'
141
+ ) {
142
+ return true;
143
+ }
144
+
145
+ if (
146
+ typeof candidate.message === 'string' &&
147
+ hasRecoverableMaxTransferErrorMessage(candidate.message)
148
+ ) {
149
+ return true;
150
+ }
151
+
152
+ stack.push(candidate.cause, candidate.error);
153
+ }
154
+
155
+ return false;
156
+ }
157
+
74
158
  /**
75
159
  * Configuration for the InventoryRebalancer.
76
160
  */
@@ -287,6 +371,10 @@ export class InventoryRebalancer implements IInventoryRebalancer {
287
371
  return total;
288
372
  }
289
373
 
374
+ private formatLocalAmount(amount: bigint, token: Token): string {
375
+ return fromWei(amount.toString(), token.decimals);
376
+ }
377
+
290
378
  /**
291
379
  * Get the effective available inventory for a chain, accounting for
292
380
  * inventory already consumed during this execution cycle.
@@ -532,12 +620,18 @@ export class InventoryRebalancer implements IInventoryRebalancer {
532
620
  {
533
621
  strategyRoute: `${origin} (surplus) → ${destination} (deficit)`,
534
622
  executionRoute: `transferRemote FROM ${destination} TO ${origin}`,
535
- amount: amount.toString(),
623
+ canonicalAmount: amount.toString(),
536
624
  intentId: intent.id,
537
625
  },
538
626
  'Executing inventory route',
539
627
  );
540
628
 
629
+ const sourceToken = this.getTokenForChain(destination);
630
+ assert(sourceToken, `No token found for source chain: ${destination}`);
631
+ const requestedLocalAmount = denormalizeToLocal(amount, sourceToken);
632
+ const executionSender = this.getInventorySignerAddress(destination);
633
+ const executionRecipient = this.getInventorySignerAddress(origin);
634
+
541
635
  // Check available inventory on the DESTINATION (deficit) chain
542
636
  // We need inventory here because transferRemote is called FROM this chain
543
637
  const availableInventory = this.getEffectiveAvailableInventory(destination);
@@ -546,9 +640,15 @@ export class InventoryRebalancer implements IInventoryRebalancer {
546
640
  {
547
641
  checkingChain: destination,
548
642
  availableInventory: availableInventory.toString(),
549
- availableInventoryEth: (Number(availableInventory) / 1e18).toFixed(6),
550
- requiredAmount: amount.toString(),
551
- requiredAmountEth: (Number(amount) / 1e18).toFixed(6),
643
+ availableInventoryFormatted: this.formatLocalAmount(
644
+ availableInventory,
645
+ sourceToken,
646
+ ),
647
+ requiredAmount: requestedLocalAmount.toString(),
648
+ requiredAmountFormatted: this.formatLocalAmount(
649
+ requestedLocalAmount,
650
+ sourceToken,
651
+ ),
552
652
  },
553
653
  'Checking effective inventory on destination (deficit) chain',
554
654
  );
@@ -559,15 +659,72 @@ export class InventoryRebalancer implements IInventoryRebalancer {
559
659
  destination, // FROM chain (where transferRemote is called)
560
660
  origin, // TO chain (where Hyperlane message goes)
561
661
  availableInventory,
562
- amount,
662
+ requestedLocalAmount,
563
663
  this.multiProvider,
564
664
  this.warpCore.multiProvider,
565
665
  this.getTokenForChain.bind(this),
566
- this.getInventorySignerAddress(destination),
666
+ executionSender,
567
667
  isNativeTokenStandard,
568
668
  this.logger,
569
669
  );
570
- const { maxTransferable, minViableTransfer } = costs;
670
+ const { minViableTransfer } = costs;
671
+ let maxTransferable = costs.maxTransferable;
672
+
673
+ if (!isNativeTokenStandard(sourceToken.standard)) {
674
+ if (availableInventory === 0n) {
675
+ maxTransferable = 0n;
676
+ this.logger.debug(
677
+ {
678
+ fromChain: destination,
679
+ toChain: origin,
680
+ requestedAmount: requestedLocalAmount.toString(),
681
+ },
682
+ 'Skipping fee-aware max transferable probe because destination inventory is zero',
683
+ );
684
+ } else {
685
+ try {
686
+ const feeAwareMaxTransfer = await this.warpCore.getMaxTransferAmount({
687
+ balance: sourceToken.amount(availableInventory),
688
+ destination: origin,
689
+ sender: executionSender,
690
+ recipient: executionRecipient,
691
+ });
692
+
693
+ maxTransferable =
694
+ feeAwareMaxTransfer.amount < requestedLocalAmount
695
+ ? feeAwareMaxTransfer.amount
696
+ : requestedLocalAmount;
697
+
698
+ this.logger.debug(
699
+ {
700
+ fromChain: destination,
701
+ toChain: origin,
702
+ availableInventory: availableInventory.toString(),
703
+ requestedAmount: requestedLocalAmount.toString(),
704
+ feeAwareMaxTransferable: maxTransferable.toString(),
705
+ },
706
+ 'Calculated fee-aware max transferable amount for non-native route',
707
+ );
708
+ } catch (error) {
709
+ if (!isRecoverableMaxTransferProbeError(error)) {
710
+ throw error;
711
+ }
712
+
713
+ maxTransferable = 0n;
714
+ this.logger.warn(
715
+ {
716
+ fromChain: destination,
717
+ toChain: origin,
718
+ availableInventory: availableInventory.toString(),
719
+ requestedAmount: requestedLocalAmount.toString(),
720
+ error: error instanceof Error ? error.message : String(error),
721
+ intentId: intent.id,
722
+ },
723
+ 'Fee-aware max transferable probe failed due to insufficient balance, falling back to external bridge',
724
+ );
725
+ }
726
+ }
727
+ }
571
728
 
572
729
  // Calculate total inventory across all chains
573
730
  // Note: consumedInventory tracking is handled separately within this cycle
@@ -577,24 +734,36 @@ export class InventoryRebalancer implements IInventoryRebalancer {
577
734
  {
578
735
  fromChain: destination,
579
736
  toChain: origin,
580
- availableInventoryEth: (Number(availableInventory) / 1e18).toFixed(6),
581
- requestedAmountEth: (Number(amount) / 1e18).toFixed(6),
582
- maxTransferableEth: (Number(maxTransferable) / 1e18).toFixed(6),
583
- minViableTransferEth: (Number(minViableTransfer) / 1e18).toFixed(6),
584
- totalInventoryEth: (Number(totalInventory) / 1e18).toFixed(6),
585
- canFullyFulfill: maxTransferable >= amount,
737
+ availableInventoryFormatted: this.formatLocalAmount(
738
+ availableInventory,
739
+ sourceToken,
740
+ ),
741
+ requestedAmountFormatted: this.formatLocalAmount(
742
+ requestedLocalAmount,
743
+ sourceToken,
744
+ ),
745
+ maxTransferableFormatted: this.formatLocalAmount(
746
+ maxTransferable,
747
+ sourceToken,
748
+ ),
749
+ minViableTransferFormatted: this.formatLocalAmount(
750
+ minViableTransfer,
751
+ sourceToken,
752
+ ),
753
+ canFullyFulfill: maxTransferable >= requestedLocalAmount,
586
754
  canPartialFulfill: maxTransferable >= minViableTransfer,
755
+ totalInventory: totalInventory.toString(),
587
756
  },
588
757
  'Calculated max transferable amount with cost-based threshold',
589
758
  );
590
759
 
591
760
  // Early exit: If remaining amount is below minViableTransfer, complete the intent
592
761
  // This prevents infinite loops when the remaining amount is too small to economically bridge
593
- if (amount < minViableTransfer) {
762
+ if (requestedLocalAmount < minViableTransfer) {
594
763
  this.logger.info(
595
764
  {
596
765
  intentId: intent.id,
597
- amount: amount.toString(),
766
+ amount: requestedLocalAmount.toString(),
598
767
  minViableTransfer: minViableTransfer.toString(),
599
768
  },
600
769
  'Remaining amount below minViableTransfer, completing intent with acceptable loss',
@@ -615,227 +784,291 @@ export class InventoryRebalancer implements IInventoryRebalancer {
615
784
  ...route,
616
785
  origin: destination, // transferRemote called FROM here
617
786
  destination: origin, // Hyperlane message goes TO here
787
+ amount: requestedLocalAmount,
618
788
  };
619
789
 
620
- if (maxTransferable >= amount) {
790
+ if (maxTransferable >= requestedLocalAmount) {
621
791
  // Sufficient inventory on destination - execute transferRemote directly
792
+ const fulfilledCanonicalAmount = normalizeToCanonical(
793
+ requestedLocalAmount,
794
+ sourceToken,
795
+ );
622
796
  const result = await this.executeTransferRemote(
623
797
  swappedRoute,
624
798
  intent,
625
- costs.gasQuote!,
799
+ fulfilledCanonicalAmount,
626
800
  );
627
801
  // Return original strategy route in result (not the swapped execution route)
628
802
  return { ...result, route };
629
803
  } else if (maxTransferable > 0n && maxTransferable >= minViableTransfer) {
630
804
  // Partial transfer: Transfer available inventory when economically viable
631
- const partialSwappedRoute: InventoryRoute = {
632
- ...swappedRoute,
633
- amount: maxTransferable,
634
- };
635
- const result = await this.executeTransferRemote(
636
- partialSwappedRoute,
637
- intent,
638
- costs.gasQuote!,
639
- );
640
-
641
- this.logger.info(
642
- {
643
- intentId: intent.id,
644
- partialAmount: maxTransferable.toString(),
645
- requestedAmount: amount.toString(),
646
- remainingAmount: (amount - maxTransferable).toString(),
647
- },
648
- 'Executed partial inventory deposit, remaining will be handled in future cycles',
805
+ const alignedExecution = alignLocalToCanonical(
806
+ maxTransferable,
807
+ sourceToken,
649
808
  );
650
-
651
- // Return original strategy route in result (not the swapped execution route)
652
- return { ...result, route };
653
- } else {
654
- // Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
655
- this.logger.info(
656
- {
657
- targetChain: destination,
658
- maxTransferable: maxTransferable.toString(),
659
- minViableTransfer: minViableTransfer.toString(),
660
- costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
661
- intentId: intent.id,
662
- },
663
- 'Inventory below cost-based threshold on destination, triggering LiFi movement',
664
- );
665
-
666
- // Get all available source chains with raw inventory
667
- const allSources = this.selectAllSourceChains(destination);
668
-
669
- if (allSources.length === 0) {
670
- this.logger.warn(
809
+ if (alignedExecution.messageAmount === 0n) {
810
+ this.logger.info(
671
811
  {
672
- origin,
673
- destination,
674
- amount: amount.toString(),
675
812
  intentId: intent.id,
813
+ maxTransferable: maxTransferable.toString(),
676
814
  },
677
- 'No inventory available on any monitored chain',
815
+ 'Skipping partial transferRemote because available local amount cannot produce canonical progress',
678
816
  );
679
-
680
- return {
681
- route,
682
- success: false,
683
- error: 'No inventory available on any monitored chain',
817
+ } else {
818
+ const partialSwappedRoute: InventoryRoute = {
819
+ ...swappedRoute,
820
+ amount: alignedExecution.localAmount,
684
821
  };
685
- }
686
-
687
- // NEW: Calculate max viable amount for each source chain
688
- // This uses the quote API to determine gas costs upfront
689
- const viableSources: Array<{ chain: ChainName; maxViable: bigint }> = [];
690
-
691
- for (const source of allSources) {
692
- const maxViable = await this.calculateMaxViableBridgeAmount(
693
- source.chain,
694
- destination,
695
- source.availableAmount,
696
- route.externalBridge,
822
+ const result = await this.executeTransferRemote(
823
+ partialSwappedRoute,
824
+ intent,
825
+ alignedExecution.messageAmount,
697
826
  );
698
827
 
699
- if (maxViable > 0n) {
700
- viableSources.push({ chain: source.chain, maxViable });
701
- }
702
- }
703
-
704
- // Sort by max viable descending (bridge from largest sources first)
705
- viableSources.sort((a, b) => (a.maxViable > b.maxViable ? -1 : 1));
706
-
707
- if (viableSources.length === 0) {
708
- this.logger.warn(
828
+ this.logger.info(
709
829
  {
710
- targetChain: destination,
711
- sourcesChecked: allSources.length,
712
830
  intentId: intent.id,
831
+ partialAmount: alignedExecution.localAmount.toString(),
832
+ partialAmountCanonical: alignedExecution.messageAmount.toString(),
833
+ requestedAmount: requestedLocalAmount.toString(),
834
+ requestedAmountCanonical: amount.toString(),
835
+ remainingAmountCanonical: (amount > alignedExecution.messageAmount
836
+ ? amount - alignedExecution.messageAmount
837
+ : 0n
838
+ ).toString(),
713
839
  },
714
- 'No viable bridge sources - all chains have insufficient inventory or high gas costs',
840
+ 'Executed partial inventory deposit, remaining will be handled in future cycles',
715
841
  );
716
842
 
717
- return {
718
- route,
719
- success: false,
720
- error: 'No viable bridge sources available',
721
- };
843
+ // Return original strategy route in result (not the swapped execution route)
844
+ return { ...result, route };
722
845
  }
846
+ }
847
+
848
+ // Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
849
+ this.logger.info(
850
+ {
851
+ targetChain: destination,
852
+ maxTransferable: maxTransferable.toString(),
853
+ minViableTransfer: minViableTransfer.toString(),
854
+ costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
855
+ intentId: intent.id,
856
+ },
857
+ 'Inventory below cost-based threshold on destination, triggering LiFi movement',
858
+ );
859
+
860
+ // Get all available source chains with raw inventory
861
+ const allSources = this.selectAllSourceChains(destination);
862
+
863
+ if (allSources.length === 0) {
864
+ this.logger.warn(
865
+ {
866
+ origin,
867
+ destination,
868
+ amount: requestedLocalAmount.toString(),
869
+ intentId: intent.id,
870
+ },
871
+ 'No inventory available on any monitored chain',
872
+ );
723
873
 
724
- // Create bridge plans using VIABLE amounts (gas already accounted for)
725
- const targetWithBuffer =
726
- ((amount + costs.totalCost) * (100n + BRIDGE_BUFFER_PERCENT)) / 100n;
727
- const bridgePlans: Array<{ chain: ChainName; amount: bigint }> = [];
728
- let totalPlanned = 0n;
874
+ return {
875
+ route,
876
+ success: false,
877
+ error: 'No inventory available on any monitored chain',
878
+ };
879
+ }
729
880
 
730
- for (const source of viableSources) {
731
- if (totalPlanned >= targetWithBuffer) break;
881
+ // Calculate source capacities in destination-local units.
882
+ const viableSources: Array<{
883
+ chain: ChainName;
884
+ maxSourceInput: bigint;
885
+ maxTargetOutput: bigint;
886
+ }> = [];
732
887
 
733
- const remaining = targetWithBuffer - totalPlanned;
734
- const amountFromSource =
735
- source.maxViable >= remaining ? remaining : source.maxViable; // Already gas-adjusted!
888
+ for (const source of allSources) {
889
+ const capacity = await this.calculateBridgeCapacity(
890
+ source.chain,
891
+ destination,
892
+ source.availableAmount,
893
+ route.externalBridge,
894
+ );
736
895
 
737
- bridgePlans.push({
738
- chain: source.chain,
739
- amount: amountFromSource,
740
- });
741
- totalPlanned += amountFromSource;
896
+ if (capacity.maxTargetOutput > 0n) {
897
+ viableSources.push({ chain: source.chain, ...capacity });
742
898
  }
899
+ }
743
900
 
744
- this.logger.info(
901
+ // Sort by destination output descending.
902
+ viableSources.sort((a, b) =>
903
+ a.maxTargetOutput > b.maxTargetOutput ? -1 : 1,
904
+ );
905
+
906
+ if (viableSources.length === 0) {
907
+ this.logger.warn(
745
908
  {
746
909
  targetChain: destination,
747
- viableSources: viableSources.map((s) => ({
748
- chain: s.chain,
749
- maxViable: s.maxViable.toString(),
750
- maxViableEth: (Number(s.maxViable) / 1e18).toFixed(6),
751
- })),
752
- bridgePlans: bridgePlans.map((p) => ({
753
- chain: p.chain,
754
- amount: p.amount.toString(),
755
- amountEth: (Number(p.amount) / 1e18).toFixed(6),
756
- })),
757
- totalPlanned: totalPlanned.toString(),
758
- targetWithBuffer: targetWithBuffer.toString(),
910
+ sourcesChecked: allSources.length,
759
911
  intentId: intent.id,
760
912
  },
761
- 'Created bridge plans using gas-adjusted viable amounts',
913
+ 'No viable bridge sources - all chains have insufficient inventory or high gas costs',
762
914
  );
763
915
 
764
- // Execute all bridges in parallel
765
- const bridgeResults = await Promise.allSettled(
766
- bridgePlans.map((plan) =>
767
- this.executeInventoryMovement(
768
- plan.chain,
769
- destination,
770
- plan.amount,
771
- intent,
772
- route.externalBridge,
773
- ),
916
+ return {
917
+ route,
918
+ success: false,
919
+ error: 'No viable bridge sources available',
920
+ };
921
+ }
922
+
923
+ // Create bridge plans using destination-local output amounts.
924
+ const shortfall =
925
+ requestedLocalAmount > availableInventory
926
+ ? requestedLocalAmount - availableInventory
927
+ : 0n;
928
+ const targetWithBuffer =
929
+ ((shortfall + costs.totalCost) * (100n + BRIDGE_BUFFER_PERCENT)) / 100n;
930
+ const bridgePlans: Array<{
931
+ chain: ChainName;
932
+ maxSourceInput: bigint;
933
+ targetOutput: bigint;
934
+ quoteMode: BridgeQuoteMode;
935
+ }> = [];
936
+ let totalPlanned = 0n;
937
+
938
+ for (const source of viableSources) {
939
+ if (totalPlanned >= targetWithBuffer) break;
940
+
941
+ const remaining = targetWithBuffer - totalPlanned;
942
+ const targetOutput =
943
+ source.maxTargetOutput >= remaining
944
+ ? remaining
945
+ : source.maxTargetOutput;
946
+ const quoteMode: BridgeQuoteMode =
947
+ source.maxTargetOutput > remaining ? 'reverse' : 'forward';
948
+
949
+ bridgePlans.push({
950
+ chain: source.chain,
951
+ maxSourceInput: source.maxSourceInput,
952
+ targetOutput,
953
+ quoteMode,
954
+ });
955
+ totalPlanned += targetOutput;
956
+ }
957
+
958
+ this.logger.info(
959
+ {
960
+ targetChain: destination,
961
+ viableSources: viableSources.map((s) => ({
962
+ chain: s.chain,
963
+ maxSourceInput: s.maxSourceInput.toString(),
964
+ maxTargetOutput: s.maxTargetOutput.toString(),
965
+ })),
966
+ bridgePlans: bridgePlans.map((p) => ({
967
+ chain: p.chain,
968
+ maxSourceInput: p.maxSourceInput.toString(),
969
+ targetOutput: p.targetOutput.toString(),
970
+ quoteMode: p.quoteMode,
971
+ })),
972
+ totalPlanned: totalPlanned.toString(),
973
+ shortfall: shortfall.toString(),
974
+ targetWithBuffer: targetWithBuffer.toString(),
975
+ intentId: intent.id,
976
+ },
977
+ 'Created bridge plans using gas-adjusted viable amounts',
978
+ );
979
+
980
+ // Execute all bridges in parallel
981
+ const bridgeResults = await Promise.allSettled(
982
+ bridgePlans.map((plan) =>
983
+ this.executeInventoryMovement(
984
+ plan.chain,
985
+ destination,
986
+ plan.targetOutput,
987
+ plan.maxSourceInput,
988
+ plan.quoteMode,
989
+ intent,
990
+ route.externalBridge,
774
991
  ),
775
- );
992
+ ),
993
+ );
776
994
 
777
- // Process results
778
- let successCount = 0;
779
- let totalBridged = 0n;
780
- const failedErrors: string[] = [];
995
+ // Process results
996
+ let successCount = 0;
997
+ let totalQuotedOutputMin = 0n;
998
+ const failedErrors: string[] = [];
781
999
 
782
- for (let i = 0; i < bridgeResults.length; i++) {
783
- const result = bridgeResults[i];
784
- const plan = bridgePlans[i];
1000
+ for (let i = 0; i < bridgeResults.length; i++) {
1001
+ const result = bridgeResults[i];
1002
+ const plan = bridgePlans[i];
785
1003
 
786
- if (result.status === 'fulfilled' && result.value.success) {
787
- successCount++;
788
- totalBridged += plan.amount;
789
- this.logger.info(
790
- {
791
- sourceChain: plan.chain,
792
- amount: plan.amount.toString(),
793
- txHash: result.value.txHash,
794
- },
795
- 'Inventory movement succeeded',
796
- );
797
- } else {
798
- const error =
799
- result.status === 'rejected'
800
- ? result.reason?.message
801
- : result.value.error;
802
- if (error) {
803
- failedErrors.push(`${plan.chain}: ${error}`);
1004
+ if (result.status === 'fulfilled' && result.value.success) {
1005
+ successCount++;
1006
+ totalQuotedOutputMin += result.value.quotedOutputMin;
1007
+ this.logger.info(
1008
+ {
1009
+ sourceChain: plan.chain,
1010
+ plannedTargetOutput: plan.targetOutput.toString(),
1011
+ quotedOutput: result.value.quotedOutput.toString(),
1012
+ quotedOutputMin: result.value.quotedOutputMin.toString(),
1013
+ quoteModeUsed: result.value.quoteModeUsed,
1014
+ txHash: result.value.txHash,
1015
+ },
1016
+ 'Inventory movement succeeded',
1017
+ );
1018
+ } else {
1019
+ let error: string | undefined;
1020
+ if (result.status === 'rejected') {
1021
+ if (result.reason instanceof Error) {
1022
+ error = result.reason.message;
1023
+ } else if (typeof result.reason === 'string') {
1024
+ error = result.reason;
1025
+ } else {
1026
+ try {
1027
+ error = JSON.stringify(result.reason) ?? String(result.reason);
1028
+ } catch {
1029
+ error = String(result.reason);
1030
+ }
804
1031
  }
805
- this.logger.warn(
806
- {
807
- sourceChain: plan.chain,
808
- amount: plan.amount.toString(),
809
- error,
810
- },
811
- 'Inventory movement failed',
812
- );
1032
+ } else if (!result.value.success) {
1033
+ error = result.value.error;
813
1034
  }
1035
+ if (error) {
1036
+ failedErrors.push(`${plan.chain}: ${error}`);
1037
+ }
1038
+ this.logger.warn(
1039
+ {
1040
+ sourceChain: plan.chain,
1041
+ plannedTargetOutput: plan.targetOutput.toString(),
1042
+ error,
1043
+ },
1044
+ 'Inventory movement failed',
1045
+ );
814
1046
  }
1047
+ }
815
1048
 
816
- if (successCount === 0) {
817
- const errorDetails =
818
- failedErrors.length > 0 ? ` (${failedErrors.join('; ')})` : '';
819
- return {
820
- route,
821
- success: false,
822
- error: `All inventory movements failed${errorDetails}`,
823
- };
824
- }
1049
+ if (successCount === 0) {
1050
+ const errorDetails =
1051
+ failedErrors.length > 0 ? ` (${failedErrors.join('; ')})` : '';
1052
+ return {
1053
+ route,
1054
+ success: false,
1055
+ error: `All inventory movements failed${errorDetails}`,
1056
+ };
1057
+ }
825
1058
 
826
- this.logger.info(
827
- {
828
- targetChain: destination,
829
- successCount,
830
- totalBridged: totalBridged.toString(),
831
- targetAmount: amount.toString(),
832
- intentId: intent.id,
833
- },
834
- 'Parallel inventory movements completed, transferRemote will execute after bridges complete',
835
- );
1059
+ this.logger.info(
1060
+ {
1061
+ targetChain: destination,
1062
+ successCount,
1063
+ totalQuotedOutputMin: totalQuotedOutputMin.toString(),
1064
+ targetAmount: requestedLocalAmount.toString(),
1065
+ targetAmountCanonical: amount.toString(),
1066
+ intentId: intent.id,
1067
+ },
1068
+ 'Parallel inventory movements completed, transferRemote will execute after bridges complete',
1069
+ );
836
1070
 
837
- return { route, success: true };
838
- }
1071
+ return { route, success: true };
839
1072
  }
840
1073
 
841
1074
  /**
@@ -851,12 +1084,11 @@ export class InventoryRebalancer implements IInventoryRebalancer {
851
1084
  *
852
1085
  * @param route - The transfer route (swapped direction)
853
1086
  * @param intent - The rebalance intent being executed
854
- * @param gasQuote - Pre-calculated gas quote from calculateTransferCosts
855
1087
  */
856
1088
  private async executeTransferRemote(
857
1089
  route: InventoryRoute,
858
1090
  intent: RebalanceIntent,
859
- gasQuote: InterchainGasQuote,
1091
+ fulfilledCanonicalAmount: bigint,
860
1092
  ): Promise<InventoryExecutionResult> {
861
1093
  const { origin, destination, amount } = route;
862
1094
 
@@ -868,46 +1100,16 @@ export class InventoryRebalancer implements IInventoryRebalancer {
868
1100
  const destinationDomain = this.multiProvider.getDomainId(destination);
869
1101
 
870
1102
  this.logger.debug(
871
- {
872
- origin,
873
- destination,
874
- amount: amount.toString(),
875
- gasQuote: {
876
- igpQuote: gasQuote.igpQuote.amount.toString(),
877
- tokenFeeQuote: gasQuote.tokenFeeQuote?.amount?.toString() ?? 'none',
878
- },
879
- },
880
- 'Using pre-calculated gas quote for transferRemote',
1103
+ { origin, destination, amount: amount.toString() },
1104
+ 'Building transferRemote transactions for exact execution amount',
881
1105
  );
882
1106
 
883
- // Convert pre-calculated gas quote to TokenAmount for WarpCore
884
- const originChainMetadata = this.multiProvider.getChainMetadata(origin);
885
- const igpAddressOrDenom = gasQuote.igpQuote.addressOrDenom;
886
- const igpToken =
887
- !igpAddressOrDenom || isZeroishAddress(igpAddressOrDenom)
888
- ? Token.FromChainMetadataNativeToken(originChainMetadata)
889
- : this.warpCore.findToken(origin, igpAddressOrDenom);
890
- assert(igpToken, `IGP fee token ${igpAddressOrDenom} is unknown`);
891
- const interchainFee = new TokenAmount(gasQuote.igpQuote.amount, igpToken);
892
-
893
- let tokenFeeQuote: TokenAmount | undefined;
894
- if (gasQuote.tokenFeeQuote?.amount) {
895
- const feeAddress = gasQuote.tokenFeeQuote.addressOrDenom;
896
- const feeToken =
897
- !feeAddress || isZeroishAddress(feeAddress)
898
- ? Token.FromChainMetadataNativeToken(originChainMetadata)
899
- : originToken;
900
- tokenFeeQuote = new TokenAmount(gasQuote.tokenFeeQuote.amount, feeToken);
901
- }
902
-
903
1107
  const originTokenAmount = originToken.amount(amount);
904
1108
  const transferTxs = await this.warpCore.getTransferRemoteTxs({
905
1109
  originTokenAmount,
906
1110
  destination,
907
1111
  sender: this.getInventorySignerAddress(origin),
908
1112
  recipient: this.getInventorySignerAddress(destination),
909
- interchainFee,
910
- tokenFeeQuote,
911
1113
  });
912
1114
  assert(
913
1115
  transferTxs.length > 0,
@@ -967,7 +1169,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
967
1169
  intentId: intent.id,
968
1170
  origin: this.multiProvider.getDomainId(origin),
969
1171
  destination: destinationDomain,
970
- amount,
1172
+ amount: fulfilledCanonicalAmount,
971
1173
  type: 'inventory_deposit',
972
1174
  txHash: transferTxHash,
973
1175
  messageId,
@@ -1024,6 +1226,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1024
1226
  ): MultiProtocolSignerSignerAccountInfo {
1025
1227
  void chain;
1026
1228
  switch (protocol) {
1229
+ case ProtocolType.Tron:
1027
1230
  case ProtocolType.Ethereum:
1028
1231
  return { protocol, privateKey: ensure0x(key) };
1029
1232
  case ProtocolType.Sealevel:
@@ -1063,7 +1266,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1063
1266
  try {
1064
1267
  const protocol = this.getProtocolForChain(origin);
1065
1268
 
1066
- if (protocol === ProtocolType.Ethereum) {
1269
+ if (isEVMLike(protocol)) {
1067
1270
  const provider =
1068
1271
  this.warpCore.multiProvider.getEthersV5Provider(origin);
1069
1272
  const receipt = await provider.getTransactionReceipt(txHash);
@@ -1076,7 +1279,9 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1076
1279
  this.warpCore.multiProvider.getSolanaWeb3Provider(origin);
1077
1280
  const receipt = await provider.getTransaction(txHash, {
1078
1281
  commitment: 'confirmed',
1079
- maxSupportedTransactionVersion: 0,
1282
+ maxSupportedTransactionVersion:
1283
+ this.warpCore.multiProvider.getChainMetadata(origin)
1284
+ .maxSupportedTransactionVersion ?? 0,
1080
1285
  });
1081
1286
  if (!receipt) return undefined;
1082
1287
  return { type: ProviderType.SolanaWeb3, receipt };
@@ -1122,40 +1327,30 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1122
1327
  }
1123
1328
 
1124
1329
  /**
1125
- * Calculate the maximum amount that can be bridged from a source chain.
1126
- * Uses LiFi quote to determine gas costs, applies 20x multiplier buffer.
1127
- * Returns 0 if gas exceeds 10% of inventory (not economically viable).
1128
- *
1129
- * This is the key method for the gas-aware planning approach:
1130
- * - Gets a quote for the full raw inventory to determine actual gas costs
1131
- * - Applies conservative 20x buffer (LiFi underestimates by ~14x historically)
1132
- * - Returns 0 if gas > 10% of inventory (not worth bridging)
1133
- * - Returns inventory - estimatedGas if viable
1330
+ * Calculate the bridge capacity from a source chain in destination-local units.
1331
+ * Uses LiFi quotes to conservatively estimate the destination output available
1332
+ * from the source chain's current local inventory.
1134
1333
  *
1135
- * @param sourceChain - Chain to bridge from
1136
- * @param targetChain - Chain to bridge to
1137
- * @param rawInventory - Raw available inventory on source chain
1138
- * @param externalBridgeType - External bridge type to use
1139
- * @returns Maximum viable bridge amount (0 if not viable)
1334
+ * For native-token sources, gas is reserved from the source inventory and the
1335
+ * output capacity is re-quoted from the remaining source input.
1140
1336
  */
1141
- private async calculateMaxViableBridgeAmount(
1337
+ private async calculateBridgeCapacity(
1142
1338
  sourceChain: ChainName,
1143
1339
  targetChain: ChainName,
1144
1340
  rawInventory: bigint,
1145
1341
  externalBridgeType: ExternalBridgeType,
1146
- ): Promise<bigint> {
1342
+ ): Promise<BridgeCapacity> {
1147
1343
  const sourceToken = this.getTokenForChain(sourceChain);
1148
1344
  const targetToken = this.getTokenForChain(targetChain);
1149
-
1150
- if (!sourceToken || !targetToken) return 0n;
1151
-
1152
- // Only applies to native tokens (need gas from same balance)
1153
- if (!isNativeTokenStandard(sourceToken.standard)) {
1154
- return rawInventory; // ERC20s don't compete with gas
1155
- }
1345
+ assert(sourceToken, `No token found for source chain: ${sourceChain}`);
1346
+ assert(targetToken, `No token found for target chain: ${targetChain}`);
1156
1347
 
1157
1348
  // Convert HypNative token addresses to the external bridge's native token representation
1158
- const fromTokenAddress = this.getNativeTokenAddress(externalBridgeType);
1349
+ const fromTokenAddress = getExternalBridgeTokenAddress(
1350
+ sourceToken,
1351
+ externalBridgeType,
1352
+ this.getNativeTokenAddress.bind(this),
1353
+ );
1159
1354
  const toTokenAddress = getExternalBridgeTokenAddress(
1160
1355
  targetToken,
1161
1356
  externalBridgeType,
@@ -1167,7 +1362,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1167
1362
 
1168
1363
  try {
1169
1364
  const externalBridge = this.getExternalBridge(externalBridgeType);
1170
- const quote = await externalBridge.quote({
1365
+ const initialQuote = await externalBridge.quote({
1171
1366
  fromChain: sourceChainId,
1172
1367
  toChain: targetChainId,
1173
1368
  fromToken: fromTokenAddress,
@@ -1177,48 +1372,58 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1177
1372
  toAddress: this.getInventorySignerAddress(targetChain),
1178
1373
  });
1179
1374
 
1180
- // Apply 20x multiplier on quoted gas (LiFi underestimates by ~14x)
1181
- const estimatedGas = quote.gasCosts * GAS_COST_MULTIPLIER;
1375
+ let maxSourceInput = rawInventory;
1376
+ let outputQuote = initialQuote;
1182
1377
 
1183
- // Viability check: gas should not exceed 10% of inventory
1184
- const maxGasThreshold = rawInventory / MAX_GAS_PERCENT_THRESHOLD;
1185
- if (estimatedGas > maxGasThreshold) {
1186
- this.logger.info(
1187
- {
1188
- sourceChain,
1189
- targetChain,
1190
- rawInventory: rawInventory.toString(),
1191
- rawInventoryEth: (Number(rawInventory) / 1e18).toFixed(6),
1192
- quotedGas: quote.gasCosts.toString(),
1193
- estimatedGas: estimatedGas.toString(),
1194
- estimatedGasEth: (Number(estimatedGas) / 1e18).toFixed(6),
1195
- maxGasThreshold: maxGasThreshold.toString(),
1196
- gasPercent: `${(Number(estimatedGas) * 100) / Number(rawInventory)}%`,
1197
- },
1198
- 'Bridge not viable - gas cost exceeds 10% of inventory',
1199
- );
1200
- return 0n;
1201
- }
1378
+ if (isNativeTokenStandard(sourceToken.standard)) {
1379
+ const estimatedGas = initialQuote.gasCosts * GAS_COST_MULTIPLIER;
1380
+ const maxGasThreshold = rawInventory / MAX_GAS_PERCENT_THRESHOLD;
1381
+ if (estimatedGas > maxGasThreshold) {
1382
+ this.logger.info(
1383
+ {
1384
+ sourceChain,
1385
+ targetChain,
1386
+ rawInventory: rawInventory.toString(),
1387
+ quotedGas: initialQuote.gasCosts.toString(),
1388
+ estimatedGas: estimatedGas.toString(),
1389
+ maxGasThreshold: maxGasThreshold.toString(),
1390
+ },
1391
+ 'Bridge not viable - gas cost exceeds 10% of inventory',
1392
+ );
1393
+ return { maxSourceInput: 0n, maxTargetOutput: 0n };
1394
+ }
1395
+
1396
+ maxSourceInput = rawInventory - estimatedGas;
1397
+ if (maxSourceInput <= 0n) {
1398
+ return { maxSourceInput: 0n, maxTargetOutput: 0n };
1399
+ }
1202
1400
 
1203
- // Max viable = inventory minus estimated gas
1204
- const maxViable = rawInventory - estimatedGas;
1401
+ outputQuote = await externalBridge.quote({
1402
+ fromChain: sourceChainId,
1403
+ toChain: targetChainId,
1404
+ fromToken: fromTokenAddress,
1405
+ toToken: toTokenAddress,
1406
+ fromAmount: maxSourceInput,
1407
+ fromAddress: this.getInventorySignerAddress(sourceChain),
1408
+ toAddress: this.getInventorySignerAddress(targetChain),
1409
+ });
1410
+ }
1205
1411
 
1206
1412
  this.logger.info(
1207
1413
  {
1208
1414
  sourceChain,
1209
1415
  targetChain,
1210
1416
  rawInventory: rawInventory.toString(),
1211
- rawInventoryEth: (Number(rawInventory) / 1e18).toFixed(6),
1212
- quotedGas: quote.gasCosts.toString(),
1213
- estimatedGas: estimatedGas.toString(),
1214
- estimatedGasEth: (Number(estimatedGas) / 1e18).toFixed(6),
1215
- maxViable: maxViable.toString(),
1216
- maxViableEth: (Number(maxViable) / 1e18).toFixed(6),
1417
+ maxSourceInput: maxSourceInput.toString(),
1418
+ maxTargetOutput: outputQuote.toAmountMin.toString(),
1217
1419
  },
1218
- 'Calculated max viable bridge amount',
1420
+ 'Calculated bridge capacity',
1219
1421
  );
1220
1422
 
1221
- return maxViable;
1423
+ return {
1424
+ maxSourceInput,
1425
+ maxTargetOutput: outputQuote.toAmountMin,
1426
+ };
1222
1427
  } catch (error) {
1223
1428
  this.logger.warn(
1224
1429
  {
@@ -1226,21 +1431,24 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1226
1431
  targetChain,
1227
1432
  error: (error as Error).message,
1228
1433
  },
1229
- 'Failed to calculate max viable bridge amount, skipping chain',
1434
+ 'Failed to calculate bridge capacity, skipping chain',
1230
1435
  );
1231
- return 0n;
1436
+ return { maxSourceInput: 0n, maxTargetOutput: 0n };
1232
1437
  }
1233
1438
  }
1234
1439
 
1235
1440
  /**
1236
1441
  * Execute inventory movement from source chain to target chain via LiFi bridge.
1237
1442
  *
1238
- * IMPORTANT: The amount parameter is now the MAX VIABLE amount (gas already subtracted
1239
- * by calculateMaxViableBridgeAmount). This method trusts that the amount is pre-validated.
1443
+ * Quote mode is chosen during planning:
1444
+ * - `reverse`: request an exact target-chain output when the source has headroom
1445
+ * - `forward`: spend the source cap directly when source inventory is the limiter
1240
1446
  *
1241
1447
  * @param sourceChain - Chain to move inventory from
1242
1448
  * @param targetChain - Chain to move inventory to (origin chain for rebalancing)
1243
- * @param amount - Pre-validated amount to bridge (gas already accounted for)
1449
+ * @param targetOutputAmount - Destination-local amount to receive
1450
+ * @param maxSourceInput - Maximum source-local amount available for this plan
1451
+ * @param quoteMode - Whether to execute this bridge plan as exact-input or exact-output
1244
1452
  * @param intent - Rebalance intent for tracking
1245
1453
  * @param externalBridgeType - External bridge type to use
1246
1454
  * @returns Result with success status and optional txHash/error
@@ -1248,10 +1456,12 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1248
1456
  private async executeInventoryMovement(
1249
1457
  sourceChain: ChainName,
1250
1458
  targetChain: ChainName,
1251
- amount: bigint,
1459
+ targetOutputAmount: bigint,
1460
+ maxSourceInput: bigint,
1461
+ quoteMode: BridgeQuoteMode,
1252
1462
  intent: RebalanceIntent,
1253
1463
  externalBridgeType: ExternalBridgeType,
1254
- ): Promise<{ success: boolean; txHash?: string; error?: string }> {
1464
+ ): Promise<InventoryMovementExecutionResult> {
1255
1465
  const sourceToken = this.getTokenForChain(sourceChain);
1256
1466
  if (!sourceToken) {
1257
1467
  return {
@@ -1297,57 +1507,53 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1297
1507
  'Resolved token addresses for LiFi bridge',
1298
1508
  );
1299
1509
 
1300
- // Calculate minViableTransfer for the target chain
1301
- // If bridging less than this, the received amount won't be enough to execute transferRemote
1302
- // So we over-bridge to ensure we can complete the intent in the next cycle
1303
- const costs = await calculateTransferCosts(
1304
- targetChain, // FROM chain for transferRemote (the target of this bridge)
1305
- sourceChain, // TO chain for transferRemote (Hyperlane message destination)
1306
- amount, // availableInventory (not used for minViableTransfer calculation)
1307
- amount, // requestedAmount
1308
- this.multiProvider,
1309
- this.warpCore.multiProvider,
1310
- this.getTokenForChain.bind(this),
1311
- this.getInventorySignerAddress(targetChain),
1312
- isNativeTokenStandard,
1313
- this.logger,
1314
- );
1315
- const { minViableTransfer } = costs;
1510
+ try {
1511
+ const externalBridge = this.getExternalBridge(externalBridgeType);
1512
+ const fromAddress = this.getInventorySignerAddress(sourceChain);
1513
+ const toAddress = this.getInventorySignerAddress(targetChain);
1514
+ const quoteWithMode = async (mode: BridgeQuoteMode) =>
1515
+ externalBridge.quote({
1516
+ fromChain: sourceChainId,
1517
+ toChain: targetChainId,
1518
+ fromToken: fromTokenAddress,
1519
+ toToken: toTokenAddress,
1520
+ ...(mode === 'forward'
1521
+ ? { fromAmount: maxSourceInput }
1522
+ : { toAmount: targetOutputAmount }),
1523
+ fromAddress,
1524
+ toAddress,
1525
+ });
1316
1526
 
1317
- // If the requested amount is below minViableTransfer, adjust it up
1318
- // This ensures we bridge enough to actually complete the final transferRemote
1319
- const effectiveAmount =
1320
- amount < minViableTransfer ? minViableTransfer : amount;
1527
+ let quoteModeUsed = quoteMode;
1528
+ let quote = await quoteWithMode(quoteModeUsed);
1321
1529
 
1322
- if (effectiveAmount !== amount) {
1323
- this.logger.info(
1324
- {
1325
- originalAmount: amount.toString(),
1326
- effectiveAmount: effectiveAmount.toString(),
1327
- minViableTransfer: minViableTransfer.toString(),
1328
- originalAmountEth: (Number(amount) / 1e18).toFixed(6),
1329
- effectiveAmountEth: (Number(effectiveAmount) / 1e18).toFixed(6),
1330
- minViableTransferEth: (Number(minViableTransfer) / 1e18).toFixed(6),
1331
- adjustedUp: true,
1332
- intentId: intent.id,
1333
- },
1334
- 'Over-bridging to minViableTransfer to ensure final transferRemote can complete',
1335
- );
1336
- }
1530
+ if (quoteModeUsed === 'reverse' && quote.fromAmount > maxSourceInput) {
1531
+ this.logger.warn(
1532
+ {
1533
+ sourceChain,
1534
+ targetChain,
1535
+ plannedQuoteMode: quoteMode,
1536
+ requestedTargetOutput: targetOutputAmount.toString(),
1537
+ quotedInput: quote.fromAmount.toString(),
1538
+ maxSourceInput: maxSourceInput.toString(),
1539
+ intentId: intent.id,
1540
+ },
1541
+ 'Reverse bridge quote exceeded source capacity, retrying with forward quote',
1542
+ );
1337
1543
 
1338
- try {
1339
- const externalBridge = this.getExternalBridge(externalBridgeType);
1340
- const quote = await externalBridge.quote({
1341
- fromChain: sourceChainId,
1342
- toChain: targetChainId,
1343
- fromToken: fromTokenAddress,
1344
- toToken: toTokenAddress,
1345
- fromAmount: effectiveAmount,
1346
- fromAddress: this.getInventorySignerAddress(sourceChain),
1347
- toAddress: this.getInventorySignerAddress(targetChain),
1348
- });
1544
+ // Spend the full source cap on fallback; minor output drift is acceptable
1545
+ // and will be reconciled by later cycles rather than risking livelock.
1546
+ quoteModeUsed = 'forward';
1547
+ quote = await quoteWithMode(quoteModeUsed);
1548
+ }
1349
1549
 
1350
1550
  const inputRequired = quote.fromAmount;
1551
+ if (inputRequired > maxSourceInput) {
1552
+ return {
1553
+ success: false,
1554
+ error: `Bridge input ${inputRequired} exceeded planned source capacity ${maxSourceInput}`,
1555
+ };
1556
+ }
1351
1557
 
1352
1558
  this.logger.info(
1353
1559
  {
@@ -1355,19 +1561,35 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1355
1561
  targetChain,
1356
1562
  sourceChainId,
1357
1563
  targetChainId,
1358
- preValidatedAmount: amount.toString(),
1359
- preValidatedAmountEth: (Number(amount) / 1e18).toFixed(6),
1360
- effectiveAmount: effectiveAmount.toString(),
1361
- effectiveAmountEth: (Number(effectiveAmount) / 1e18).toFixed(6),
1564
+ requestedTargetOutput: targetOutputAmount.toString(),
1565
+ requestedTargetOutputFormatted: this.formatLocalAmount(
1566
+ targetOutputAmount,
1567
+ targetToken,
1568
+ ),
1569
+ quoteModePlanned: quoteMode,
1570
+ quoteModeUsed,
1571
+ retriedAsForward:
1572
+ quoteMode === 'reverse' && quoteModeUsed === 'forward',
1362
1573
  inputRequired: inputRequired.toString(),
1363
- expectedOutput: quote.toAmount.toString(),
1364
- expectedOutputEth: (Number(quote.toAmount) / 1e18).toFixed(6),
1574
+ inputRequiredFormatted: this.formatLocalAmount(
1575
+ inputRequired,
1576
+ sourceToken,
1577
+ ),
1578
+ quotedOutput: quote.toAmount.toString(),
1579
+ quotedOutputMin: quote.toAmountMin.toString(),
1580
+ quotedOutputFormatted: this.formatLocalAmount(
1581
+ quote.toAmount,
1582
+ targetToken,
1583
+ ),
1584
+ quotedOutputMinFormatted: this.formatLocalAmount(
1585
+ quote.toAmountMin,
1586
+ targetToken,
1587
+ ),
1365
1588
  gasCosts: quote.gasCosts.toString(),
1366
1589
  feeCosts: quote.feeCosts.toString(),
1367
1590
  intentId: intent.id,
1368
- adjustedForMinViable: effectiveAmount > amount,
1369
1591
  },
1370
- 'Executing inventory movement via LiFi with pre-validated amount',
1592
+ 'Executing inventory movement via bridge quote',
1371
1593
  );
1372
1594
 
1373
1595
  this.logger.debug(
@@ -1410,6 +1632,8 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1410
1632
  'Inventory movement transaction executed',
1411
1633
  );
1412
1634
 
1635
+ // Keep bridge consumption in source-local units; intent fulfillment only
1636
+ // advances from canonical inventory_deposit amounts after transferRemote.
1413
1637
  await this.actionTracker.createRebalanceAction({
1414
1638
  intentId: intent.id,
1415
1639
  origin: this.multiProvider.getDomainId(sourceChain),
@@ -1433,22 +1657,31 @@ export class InventoryRebalancer implements IInventoryRebalancer {
1433
1657
  'Updated consumed inventory after LiFi bridge',
1434
1658
  );
1435
1659
 
1436
- return { success: true, txHash: result.txHash };
1660
+ return {
1661
+ success: true,
1662
+ txHash: result.txHash,
1663
+ inputRequired,
1664
+ quotedOutput: quote.toAmount,
1665
+ quotedOutputMin: quote.toAmountMin,
1666
+ quoteModeUsed,
1667
+ };
1437
1668
  } catch (error) {
1669
+ const errorMessage =
1670
+ error instanceof Error ? error.message : String(error);
1438
1671
  this.logger.error(
1439
1672
  {
1440
1673
  sourceChain,
1441
1674
  targetChain,
1442
- amount: amount.toString(),
1675
+ amount: targetOutputAmount.toString(),
1443
1676
  intentId: intent.id,
1444
- error: (error as Error).message,
1677
+ error: errorMessage,
1445
1678
  },
1446
1679
  'Failed to execute inventory movement',
1447
1680
  );
1448
1681
 
1449
1682
  return {
1450
1683
  success: false,
1451
- error: (error as Error).message,
1684
+ error: errorMessage,
1452
1685
  };
1453
1686
  }
1454
1687
  }