@humanode-hyperlane/rebalancer 27.2.2-humanode.1 → 1029.1.0

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,8 +3,8 @@ import chaiAsPromised from 'chai-as-promised';
3
3
  import { Wallet } from 'ethers';
4
4
  import { pino } from 'pino';
5
5
  import Sinon from 'sinon';
6
- import { ProviderType, TokenStandard, WarpTxCategory, } from '@humanode-hyperlane/sdk';
7
- import { ProtocolType } from '@humanode-hyperlane/utils';
6
+ import { HyperlaneCore, ProviderType, TokenStandard, WarpTxCategory, } from '@humanode-hyperlane/sdk';
7
+ import { assert, ProtocolType } from '@humanode-hyperlane/utils';
8
8
  import { ExternalBridgeType } from '../config/types.js';
9
9
  import { createMockBridgeQuote } from '../test/lifiMocks.js';
10
10
  import { InventoryRebalancer, } from './InventoryRebalancer.js';
@@ -123,6 +123,7 @@ describe('InventoryRebalancer E2E', () => {
123
123
  }),
124
124
  },
125
125
  findToken: Sinon.stub().returns(null),
126
+ getMaxTransferAmount: Sinon.stub().callsFake(async ({ balance }) => balance),
126
127
  getTransferRemoteTxs: Sinon.stub().resolves([
127
128
  {
128
129
  category: 'transfer',
@@ -230,6 +231,18 @@ describe('InventoryRebalancer E2E', () => {
230
231
  actionTracker.createRebalanceIntent.resolves(intent);
231
232
  return intent;
232
233
  }
234
+ function mockSuccessfulBridge(fromAmount, toAmount) {
235
+ bridge.quote.resolves(createMockBridgeQuote({
236
+ fromAmount,
237
+ toAmount,
238
+ toAmountMin: toAmount,
239
+ }));
240
+ bridge.execute.resolves({
241
+ txHash: '0xBridgeTxHash',
242
+ fromChain: 42161,
243
+ toChain: 1399811149,
244
+ });
245
+ }
233
246
  describe('Basic Inventory Rebalance (Sufficient Inventory)', () => {
234
247
  // NOTE: Strategy route is arbitrum (surplus) → solana (deficit)
235
248
  // But execution calls transferRemote FROM solana TO arbitrum (swapped direction)
@@ -323,10 +336,137 @@ describe('InventoryRebalancer E2E', () => {
323
336
  const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
324
337
  expect(actionParams.txHash).to.equal('0xSolanaTxHash');
325
338
  });
339
+ it('denormalizes inventory execution amounts but records canonical deposit amount', async () => {
340
+ const route = createTestRoute({ amount: 1000000n });
341
+ createTestIntent({ amount: 1000000n });
342
+ warpCore.tokens.find((t) => t.chainName === SOLANA_CHAIN).scale = {
343
+ numerator: 1n,
344
+ denominator: 1000000000000n,
345
+ };
346
+ inventoryRebalancer.setInventoryBalances({
347
+ [SOLANA_CHAIN]: 1000000000000000000n,
348
+ [ARBITRUM_CHAIN]: 0n,
349
+ });
350
+ const results = await inventoryRebalancer.rebalance([route]);
351
+ expect(results).to.have.lengthOf(1);
352
+ expect(results[0].success).to.be.true;
353
+ const txParams = warpCore.getTransferRemoteTxs.firstCall.args[0];
354
+ expect(txParams.originTokenAmount.amount).to.equal(1000000000000000000n);
355
+ const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
356
+ expect(actionParams.amount).to.equal(1000000n);
357
+ });
358
+ });
359
+ it('sendAndConfirmInventoryTx uses the EVM-like signer path for Tron txs', async () => {
360
+ const TRON_CHAIN = 'tron';
361
+ config.inventorySigners[ProtocolType.Tron] = {
362
+ address: INVENTORY_SIGNER,
363
+ key: TEST_PRIVATE_KEY,
364
+ };
365
+ const getChainMetadata = (chain) => ({
366
+ name: chain,
367
+ protocol: chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
368
+ blocks: { reorgPeriod: 2 },
369
+ rpcUrls: [{ http: 'http://127.0.0.1:9090/jsonrpc' }],
370
+ });
371
+ warpCore.multiProvider.getChainMetadata.callsFake(getChainMetadata);
372
+ multiProvider.getChainMetadata.callsFake(getChainMetadata);
373
+ warpCore.multiProvider.toMultiProvider =
374
+ Sinon.stub().returns(multiProvider);
375
+ const sendFn = inventoryRebalancer.sendAndConfirmInventoryTx.bind(inventoryRebalancer);
376
+ const result = await sendFn(TRON_CHAIN, {
377
+ category: WarpTxCategory.Transfer,
378
+ type: ProviderType.Tron,
379
+ transaction: {
380
+ to: '0xRouterAddress',
381
+ data: '0xTransferRemoteData',
382
+ value: 1000000n,
383
+ },
384
+ });
385
+ expect(result).to.deep.equal({ txHash: '0xTransferRemoteTxHash' });
386
+ expect(multiProvider.setSigner.calledOnce).to.be.true;
387
+ expect(multiProvider.sendTransaction.calledOnceWithExactly(TRON_CHAIN, {
388
+ to: '0xRouterAddress',
389
+ data: '0xTransferRemoteData',
390
+ value: 1000000n,
391
+ }, { waitConfirmations: 2 })).to.be.true;
392
+ });
393
+ it('buildSignerAccountConfig returns correct config for Tron protocol', () => {
394
+ config.inventorySigners[ProtocolType.Tron] = {
395
+ address: INVENTORY_SIGNER,
396
+ key: TEST_PRIVATE_KEY,
397
+ };
398
+ const buildFn = inventoryRebalancer.buildSignerAccountConfig.bind(inventoryRebalancer);
399
+ const result = buildFn(ProtocolType.Tron, TEST_PRIVATE_KEY, 'tron');
400
+ expect(result.protocol).to.equal(ProtocolType.Tron);
401
+ expect(result.privateKey).to.equal(TEST_PRIVATE_KEY);
402
+ // Tron uses same 0x-prefixed hex key format as Ethereum
403
+ expect(result.privateKey.startsWith('0x')).to.be.true;
404
+ });
405
+ it('getTransactionReceipt returns EthersV5 type for Tron chain', async () => {
406
+ const TRON_CHAIN = 'tron';
407
+ const mockReceipt = {
408
+ transactionHash: '0xTronReceipt',
409
+ logs: [],
410
+ };
411
+ warpCore.multiProvider.getChainMetadata.callsFake((chain) => ({
412
+ name: chain,
413
+ protocol: chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
414
+ }));
415
+ warpCore.multiProvider.getEthersV5Provider.returns({
416
+ getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
417
+ });
418
+ const getReceiptFn = inventoryRebalancer.getTransactionReceipt.bind(inventoryRebalancer);
419
+ const receipt = await getReceiptFn(TRON_CHAIN, '0xTronTxHash');
420
+ expect(receipt).to.not.be.undefined;
421
+ expect(receipt.type).to.equal(ProviderType.EthersV5);
422
+ expect(receipt.receipt).to.deep.equal(mockReceipt);
423
+ });
424
+ it('extractDispatchedMessageId returns Tron message ids via the EthersV5 path', async () => {
425
+ const TRON_CHAIN = 'tron';
426
+ const expectedMessageId = '0x1111111111111111111111111111111111111111111111111111111111111111';
427
+ const mockReceipt = {
428
+ transactionHash: '0xTronTx',
429
+ logs: [{}],
430
+ };
431
+ warpCore.multiProvider.getChainMetadata.callsFake((chain) => ({
432
+ name: chain,
433
+ protocol: chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
434
+ }));
435
+ const getDispatchedMessagesStub = Sinon.stub(HyperlaneCore, 'getDispatchedMessages').returns([{ id: expectedMessageId }]);
436
+ warpCore.multiProvider.getEthersV5Provider.returns({
437
+ getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
438
+ });
439
+ const extractFn = inventoryRebalancer.extractDispatchedMessageId.bind(inventoryRebalancer);
440
+ const messageId = await extractFn(TRON_CHAIN, '0xTronTx');
441
+ expect(messageId).to.equal(expectedMessageId);
442
+ expect(getDispatchedMessagesStub.calledOnce).to.be.true;
443
+ expect(getDispatchedMessagesStub.firstCall.firstArg).to.equal(mockReceipt);
444
+ });
445
+ it('selects the Tron signer address from a mixed multi-protocol config', () => {
446
+ config.inventorySigners[ProtocolType.Tron] = {
447
+ address: INVENTORY_SIGNER,
448
+ key: TEST_PRIVATE_KEY,
449
+ };
450
+ config.inventorySigners[ProtocolType.Sealevel] = {
451
+ address: 'SoLANAAddReSs1234567890123456789012345678',
452
+ key: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32',
453
+ };
454
+ warpCore.multiProvider.getChainMetadata.callsFake((chain) => ({
455
+ name: chain,
456
+ protocol: chain === 'tron'
457
+ ? ProtocolType.Tron
458
+ : chain === SOLANA_CHAIN
459
+ ? ProtocolType.Sealevel
460
+ : ProtocolType.Ethereum,
461
+ }));
462
+ const getInventorySignerAddress = inventoryRebalancer.getInventorySignerAddress.bind(inventoryRebalancer);
463
+ expect(getInventorySignerAddress('tron')).to.equal(INVENTORY_SIGNER);
464
+ expect(getInventorySignerAddress(SOLANA_CHAIN)).to.equal('SoLANAAddReSs1234567890123456789012345678');
326
465
  });
327
466
  describe('Partial Fulfillment (Insufficient Inventory)', () => {
328
- // Partial transfers happen when maxTransferable >= minViableTransfer
329
- // For non-native tokens (EvmHypCollateral), minViableTransfer = 0, so partial always viable
467
+ // Partial transfers happen when maxTransferable >= minViableTransfer.
468
+ // For non-native tokens (EvmHypCollateral), minViableTransfer = 0, so any
469
+ // positive fee-aware maxTransferable remains viable.
330
470
  const PARTIAL_AMOUNT = BigInt(5e15); // 0.005 ETH - above threshold
331
471
  const FULL_AMOUNT = BigInt(1e16); // 0.01 ETH
332
472
  it('executes partial transferRemote when maxTransferable >= minViableTransfer', async () => {
@@ -351,6 +491,49 @@ describe('InventoryRebalancer E2E', () => {
351
491
  const actionParams = actionTracker.createRebalanceAction.firstCall.args[0];
352
492
  expect(actionParams.amount).to.equal(PARTIAL_AMOUNT);
353
493
  });
494
+ it('uses fee-aware maxTransferable for non-native token fees', async () => {
495
+ const requestedAmount = 19998000000n;
496
+ const availableInventory = 102466n;
497
+ const safeTransferAmount = 102400n;
498
+ warpCore.getMaxTransferAmount.resolves({ amount: safeTransferAmount });
499
+ const route = createTestRoute({ amount: requestedAmount });
500
+ createTestIntent({ amount: requestedAmount });
501
+ inventoryRebalancer.setInventoryBalances({
502
+ [SOLANA_CHAIN]: availableInventory,
503
+ [ARBITRUM_CHAIN]: 0n,
504
+ });
505
+ const results = await inventoryRebalancer.rebalance([route]);
506
+ expect(results).to.have.lengthOf(1);
507
+ expect(results[0].success).to.be.true;
508
+ expect(warpCore.getMaxTransferAmount.calledOnce).to.be.true;
509
+ const maxTransferArgs = warpCore.getMaxTransferAmount.firstCall.args[0];
510
+ expect(maxTransferArgs.balance.amount).to.equal(availableInventory);
511
+ expect(maxTransferArgs.destination).to.equal(ARBITRUM_CHAIN);
512
+ const txParams = warpCore.getTransferRemoteTxs.lastCall.args[0];
513
+ expect(txParams.originTokenAmount.amount).to.equal(safeTransferAmount);
514
+ expect(txParams).to.not.have.property('interchainFee');
515
+ expect(txParams).to.not.have.property('tokenFeeQuote');
516
+ const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
517
+ expect(actionParams.amount).to.equal(safeTransferAmount);
518
+ });
519
+ it('downgrades full non-native transfers to partial when fee headroom is missing', async () => {
520
+ const requestedAmount = 102466n;
521
+ const safeTransferAmount = 102400n;
522
+ warpCore.getMaxTransferAmount.resolves({ amount: safeTransferAmount });
523
+ const route = createTestRoute({ amount: requestedAmount });
524
+ createTestIntent({ amount: requestedAmount });
525
+ inventoryRebalancer.setInventoryBalances({
526
+ [SOLANA_CHAIN]: requestedAmount,
527
+ [ARBITRUM_CHAIN]: 0n,
528
+ });
529
+ const results = await inventoryRebalancer.rebalance([route]);
530
+ expect(results).to.have.lengthOf(1);
531
+ expect(results[0].success).to.be.true;
532
+ expect(bridge.execute.called).to.be.false;
533
+ const txParams = warpCore.getTransferRemoteTxs.lastCall.args[0];
534
+ expect(txParams.originTokenAmount.amount).to.equal(safeTransferAmount);
535
+ expect(txParams.originTokenAmount.amount).to.not.equal(requestedAmount);
536
+ });
354
537
  it('intent remains in_progress after partial fulfillment', async () => {
355
538
  const route = createTestRoute({ amount: FULL_AMOUNT });
356
539
  createTestIntent({ amount: FULL_AMOUNT });
@@ -364,6 +547,42 @@ describe('InventoryRebalancer E2E', () => {
364
547
  expect(results[0].success).to.be.true;
365
548
  });
366
549
  });
550
+ describe('Fee-Aware Probe Fallback', () => {
551
+ it('bridges when non-native destination inventory is zero', async () => {
552
+ const requestedAmount = 10000000000n;
553
+ const bufferedBridgeAmount = (requestedAmount * 105n) / 100n;
554
+ const route = createTestRoute({ amount: requestedAmount });
555
+ createTestIntent({ amount: requestedAmount });
556
+ inventoryRebalancer.setInventoryBalances({
557
+ [SOLANA_CHAIN]: 0n,
558
+ [ARBITRUM_CHAIN]: 20000000000n,
559
+ });
560
+ mockSuccessfulBridge(bufferedBridgeAmount, bufferedBridgeAmount);
561
+ const results = await inventoryRebalancer.rebalance([route]);
562
+ expect(results).to.have.lengthOf(1);
563
+ expect(results[0].success).to.be.true;
564
+ expect(warpCore.getMaxTransferAmount.called).to.be.false;
565
+ expect(warpCore.getTransferRemoteTxs.called).to.be.false;
566
+ expect(bridge.execute.calledOnce).to.be.true;
567
+ const actionParams = actionTracker.createRebalanceAction.firstCall.args[0];
568
+ expect(actionParams.type).to.equal('inventory_movement');
569
+ });
570
+ it('returns failure for unrelated fee-aware probe errors', async () => {
571
+ const route = createTestRoute();
572
+ createTestIntent();
573
+ inventoryRebalancer.setInventoryBalances({
574
+ [SOLANA_CHAIN]: 1n,
575
+ [ARBITRUM_CHAIN]: 20000000000n,
576
+ });
577
+ warpCore.getMaxTransferAmount.rejects(new Error('RPC down'));
578
+ const results = await inventoryRebalancer.rebalance([route]);
579
+ expect(results).to.have.lengthOf(1);
580
+ expect(results[0].success).to.be.false;
581
+ expect(results[0].error).to.include('RPC down');
582
+ expect(bridge.execute.called).to.be.false;
583
+ expect(actionTracker.createRebalanceAction.called).to.be.false;
584
+ });
585
+ });
367
586
  describe('No Inventory Available', () => {
368
587
  it('returns failure when no inventory on destination chain and no other source available', async () => {
369
588
  const route = createTestRoute();
@@ -507,6 +726,42 @@ describe('InventoryRebalancer E2E', () => {
507
726
  // Verify: No new intent was created (existing was continued)
508
727
  expect(actionTracker.createRebalanceIntent.called).to.be.false;
509
728
  });
729
+ it('continues existing intent by bridging after recoverable fee-aware probe failure', async () => {
730
+ const existingIntent = createTestIntent({
731
+ id: 'existing-intent',
732
+ status: 'in_progress',
733
+ amount: 10000000000n,
734
+ externalBridge: ExternalBridgeType.LiFi,
735
+ });
736
+ const recoverableProbeError = new Error('Fee probe failed');
737
+ recoverableProbeError.cause = {
738
+ code: 'UNPREDICTABLE_GAS_LIMIT',
739
+ message: 'execution reverted: ERC20: transfer amount exceeds balance',
740
+ };
741
+ actionTracker.getPartiallyFulfilledInventoryIntents.resolves([
742
+ {
743
+ intent: existingIntent,
744
+ completedAmount: 0n,
745
+ remaining: 10000000000n,
746
+ hasInflightDeposit: false,
747
+ },
748
+ ]);
749
+ inventoryRebalancer.setInventoryBalances({
750
+ [SOLANA_CHAIN]: 1n,
751
+ [ARBITRUM_CHAIN]: 20000000000n,
752
+ });
753
+ warpCore.getMaxTransferAmount.rejects(recoverableProbeError);
754
+ mockSuccessfulBridge(10500000000n, 10500000000n);
755
+ const results = await inventoryRebalancer.rebalance([
756
+ createTestRoute({ amount: 5000000000n }),
757
+ ]);
758
+ expect(results).to.have.lengthOf(1);
759
+ expect(results[0].success).to.be.true;
760
+ expect(bridge.execute.calledOnce).to.be.true;
761
+ expect(actionTracker.createRebalanceIntent.called).to.be.false;
762
+ const actionParams = actionTracker.createRebalanceAction.firstCall.args[0];
763
+ expect(actionParams.type).to.equal('inventory_movement');
764
+ });
510
765
  });
511
766
  describe('Error Handling', () => {
512
767
  it('handles transaction send failure', async () => {
@@ -1190,6 +1445,56 @@ describe('InventoryRebalancer E2E', () => {
1190
1445
  // Verify: Bridge was called twice (once for each source)
1191
1446
  expect(bridge.execute.callCount).to.equal(2);
1192
1447
  });
1448
+ it('does not inflate each split bridge plan to minViableTransfer', async () => {
1449
+ const amount = 7000000000000000n;
1450
+ const perChainInventory = 6000000000000000n;
1451
+ const reservedGas = 1000000000n;
1452
+ for (const token of warpCore.tokens) {
1453
+ if (token.chainName === ARBITRUM_CHAIN ||
1454
+ token.chainName === SOLANA_CHAIN ||
1455
+ token.chainName === BASE_CHAIN) {
1456
+ token.standard = TokenStandard.EvmHypNative;
1457
+ }
1458
+ }
1459
+ const route = createTestRoute({ amount });
1460
+ createTestIntent({ amount });
1461
+ inventoryRebalancer.setInventoryBalances({
1462
+ [SOLANA_CHAIN]: 0n,
1463
+ [ARBITRUM_CHAIN]: perChainInventory,
1464
+ [BASE_CHAIN]: perChainInventory,
1465
+ });
1466
+ bridge.quote.callsFake(async (params) => createMockBridgeQuote({
1467
+ fromAmount: params.fromAmount ?? params.toAmount,
1468
+ toAmount: params.toAmount ?? params.fromAmount,
1469
+ toAmountMin: params.toAmount ?? params.fromAmount,
1470
+ requestParams: params.toAmount !== undefined
1471
+ ? { ...params, toAmount: params.toAmount }
1472
+ : { ...params, fromAmount: params.fromAmount },
1473
+ }));
1474
+ bridge.execute.resolves({
1475
+ txHash: '0xBridgeTxHash',
1476
+ fromChain: 42161,
1477
+ toChain: 1399811149,
1478
+ });
1479
+ const results = await inventoryRebalancer.rebalance([route]);
1480
+ expect(results).to.have.lengthOf(1);
1481
+ expect(results[0].success).to.be.true;
1482
+ expect(bridge.execute.callCount).to.equal(2);
1483
+ const maxPerSourceOutput = perChainInventory - reservedGas;
1484
+ const executionQuoteRequests = bridge.quote
1485
+ .getCalls()
1486
+ .slice(-2)
1487
+ .map((call) => call.args[0]);
1488
+ expect(executionQuoteRequests).to.have.lengthOf(2);
1489
+ const forwardQuoteRequests = executionQuoteRequests.filter((params) => params.fromAmount !== undefined);
1490
+ const reverseQuoteRequests = executionQuoteRequests.filter((params) => params.toAmount !== undefined);
1491
+ const forwardAmounts = forwardQuoteRequests.map((params) => params.fromAmount);
1492
+ expect(forwardQuoteRequests.length + reverseQuoteRequests.length).to.equal(2);
1493
+ expect(forwardQuoteRequests.length).to.be.greaterThan(0);
1494
+ expect(forwardAmounts.every((amount) => amount <= maxPerSourceOutput)).to
1495
+ .be.true;
1496
+ expect(reverseQuoteRequests.every((params) => params.toAmount <= maxPerSourceOutput)).to.be.true;
1497
+ });
1193
1498
  it('applies 5% buffer to total bridge amount', async () => {
1194
1499
  // Need 1 ETH -> should plan to bridge 1.05 ETH total (with 5% buffer)
1195
1500
  const amount = BigInt(1e18); // 1 ETH
@@ -1201,14 +1506,18 @@ describe('InventoryRebalancer E2E', () => {
1201
1506
  [SOLANA_CHAIN]: 0n,
1202
1507
  [ARBITRUM_CHAIN]: availableInventory,
1203
1508
  });
1204
- // Capture the quote amount from executeInventoryMovement
1205
- // (calculateMaxViableBridgeAmount doesn't quote for ERC20 tokens)
1206
- let quotedFromAmount;
1509
+ let quotedTargetOutput;
1207
1510
  bridge.quote.callsFake(async (params) => {
1208
- quotedFromAmount = params.fromAmount;
1511
+ if (params.toAmount !== undefined) {
1512
+ quotedTargetOutput = params.toAmount;
1513
+ }
1209
1514
  return createMockBridgeQuote({
1210
1515
  fromAmount: params.fromAmount ?? params.toAmount,
1211
- toAmount: params.fromAmount ?? params.toAmount,
1516
+ toAmount: params.toAmount ?? params.fromAmount,
1517
+ toAmountMin: params.toAmount ?? params.fromAmount,
1518
+ requestParams: params.toAmount !== undefined
1519
+ ? { ...params, toAmount: params.toAmount }
1520
+ : { ...params, fromAmount: params.fromAmount },
1212
1521
  });
1213
1522
  });
1214
1523
  bridge.execute.resolves({
@@ -1218,10 +1527,359 @@ describe('InventoryRebalancer E2E', () => {
1218
1527
  });
1219
1528
  await inventoryRebalancer.rebalance([route]);
1220
1529
  // Verify: 5% buffer applied (1 ETH * 1.05 = 1.05 ETH)
1221
- // The bridge plan uses pre-validated amounts (for ERC20, full inventory available)
1222
- // But the target is (amount * 105%), so if source has >= target, we bridge exactly target
1223
1530
  const expectedWithBuffer = (amount * 105n) / 100n;
1224
- expect(quotedFromAmount).to.equal(expectedWithBuffer);
1531
+ expect(quotedTargetOutput).to.equal(expectedWithBuffer);
1532
+ });
1533
+ it('bridges only the mixed-decimal shortfall after dust partial skip', async () => {
1534
+ const canonicalAmount = 1n;
1535
+ const destinationDust = 1000000000000n - 1n;
1536
+ const sourceInventory = 1000000000000n;
1537
+ warpCore.tokens.find((t) => t.chainName === SOLANA_CHAIN).scale = {
1538
+ numerator: 1n,
1539
+ denominator: 1000000000000n,
1540
+ };
1541
+ const route = createTestRoute({ amount: canonicalAmount });
1542
+ createTestIntent({ amount: canonicalAmount });
1543
+ inventoryRebalancer.setInventoryBalances({
1544
+ [SOLANA_CHAIN]: destinationDust,
1545
+ [ARBITRUM_CHAIN]: sourceInventory,
1546
+ });
1547
+ let reverseQuoteTargetOutput;
1548
+ bridge.quote.callsFake(async (params) => {
1549
+ if (params.toAmount !== undefined) {
1550
+ reverseQuoteTargetOutput = params.toAmount;
1551
+ }
1552
+ return createMockBridgeQuote({
1553
+ fromAmount: params.fromAmount ?? params.toAmount,
1554
+ toAmount: params.toAmount ?? params.fromAmount,
1555
+ toAmountMin: params.toAmount ?? params.fromAmount,
1556
+ requestParams: params.toAmount !== undefined
1557
+ ? { ...params, toAmount: params.toAmount }
1558
+ : { ...params, fromAmount: params.fromAmount },
1559
+ });
1560
+ });
1561
+ bridge.execute.resolves({
1562
+ txHash: '0xBridgeTxHash',
1563
+ fromChain: 42161,
1564
+ toChain: 1399811149,
1565
+ });
1566
+ const results = await inventoryRebalancer.rebalance([route]);
1567
+ expect(results).to.have.lengthOf(1);
1568
+ expect(results[0].success).to.be.true;
1569
+ expect(warpCore.getTransferRemoteTxs.called).to.be.false;
1570
+ expect(bridge.execute.calledOnce).to.be.true;
1571
+ expect(reverseQuoteTargetOutput).to.equal(1n);
1572
+ });
1573
+ it('plans LiFi target output in destination-local units for mixed-decimal routes', async () => {
1574
+ const canonicalAmount = 1000000n;
1575
+ const availableInventory = BigInt(2e18);
1576
+ warpCore.tokens.find((t) => t.chainName === SOLANA_CHAIN).scale = {
1577
+ numerator: 1n,
1578
+ denominator: 1000000000000n,
1579
+ };
1580
+ const route = createTestRoute({ amount: canonicalAmount });
1581
+ createTestIntent({ amount: canonicalAmount });
1582
+ inventoryRebalancer.setInventoryBalances({
1583
+ [SOLANA_CHAIN]: 0n,
1584
+ [ARBITRUM_CHAIN]: availableInventory,
1585
+ });
1586
+ let quotedTargetOutput;
1587
+ bridge.quote.callsFake(async (params) => {
1588
+ if (params.toAmount !== undefined) {
1589
+ quotedTargetOutput = params.toAmount;
1590
+ }
1591
+ return createMockBridgeQuote({
1592
+ fromAmount: params.fromAmount ?? params.toAmount,
1593
+ toAmount: params.toAmount ?? params.fromAmount,
1594
+ toAmountMin: params.toAmount ?? params.fromAmount,
1595
+ requestParams: params.toAmount !== undefined
1596
+ ? { ...params, toAmount: params.toAmount }
1597
+ : { ...params, fromAmount: params.fromAmount },
1598
+ });
1599
+ });
1600
+ bridge.execute.resolves({
1601
+ txHash: '0xBridgeTxHash',
1602
+ fromChain: 42161,
1603
+ toChain: 1399811149,
1604
+ });
1605
+ await inventoryRebalancer.rebalance([route]);
1606
+ expect(quotedTargetOutput).to.equal(1050000000000000000n);
1607
+ });
1608
+ it('retries forward execution when reverse quote exceeds planned source capacity', async () => {
1609
+ const amount = BigInt(1e18);
1610
+ const sourceInventory = BigInt(2e18);
1611
+ const targetWithBuffer = (amount * 105n) / 100n;
1612
+ const route = createTestRoute({ amount });
1613
+ createTestIntent({ amount });
1614
+ inventoryRebalancer.setInventoryBalances({
1615
+ [SOLANA_CHAIN]: 0n,
1616
+ [ARBITRUM_CHAIN]: sourceInventory,
1617
+ });
1618
+ bridge.quote
1619
+ .onFirstCall()
1620
+ .resolves(createMockBridgeQuote({
1621
+ fromAmount: sourceInventory,
1622
+ toAmount: sourceInventory,
1623
+ toAmountMin: sourceInventory,
1624
+ requestParams: {
1625
+ fromChain: 42161,
1626
+ toChain: 1399811149,
1627
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1628
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1629
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1630
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1631
+ fromAmount: sourceInventory,
1632
+ },
1633
+ }))
1634
+ .onSecondCall()
1635
+ .resolves(createMockBridgeQuote({
1636
+ fromAmount: sourceInventory + 1n,
1637
+ toAmount: targetWithBuffer,
1638
+ toAmountMin: targetWithBuffer,
1639
+ requestParams: {
1640
+ fromChain: 42161,
1641
+ toChain: 1399811149,
1642
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1643
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1644
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1645
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1646
+ toAmount: targetWithBuffer,
1647
+ },
1648
+ }))
1649
+ .onThirdCall()
1650
+ .resolves(createMockBridgeQuote({
1651
+ fromAmount: sourceInventory,
1652
+ toAmount: targetWithBuffer - 1n,
1653
+ toAmountMin: targetWithBuffer - 1n,
1654
+ requestParams: {
1655
+ fromChain: 42161,
1656
+ toChain: 1399811149,
1657
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1658
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1659
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1660
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1661
+ fromAmount: sourceInventory,
1662
+ },
1663
+ }));
1664
+ bridge.execute.resolves({
1665
+ txHash: '0xBridgeTxHash',
1666
+ fromChain: 42161,
1667
+ toChain: 1399811149,
1668
+ });
1669
+ const results = await inventoryRebalancer.rebalance([route]);
1670
+ expect(results).to.have.lengthOf(1);
1671
+ expect(results[0].success).to.be.true;
1672
+ expect(bridge.execute.calledOnce).to.be.true;
1673
+ expect(bridge.quote.callCount).to.equal(3);
1674
+ expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
1675
+ expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(sourceInventory);
1676
+ expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
1677
+ });
1678
+ it('fails when forward retry still exceeds planned source capacity', async () => {
1679
+ const amount = BigInt(1e18);
1680
+ const sourceInventory = BigInt(2e18);
1681
+ const targetWithBuffer = (amount * 105n) / 100n;
1682
+ const route = createTestRoute({ amount });
1683
+ createTestIntent({ amount });
1684
+ inventoryRebalancer.setInventoryBalances({
1685
+ [SOLANA_CHAIN]: 0n,
1686
+ [ARBITRUM_CHAIN]: sourceInventory,
1687
+ });
1688
+ bridge.quote
1689
+ .onFirstCall()
1690
+ .resolves(createMockBridgeQuote({
1691
+ fromAmount: sourceInventory,
1692
+ toAmount: sourceInventory,
1693
+ toAmountMin: sourceInventory,
1694
+ requestParams: {
1695
+ fromChain: 42161,
1696
+ toChain: 1399811149,
1697
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1698
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1699
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1700
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1701
+ fromAmount: sourceInventory,
1702
+ },
1703
+ }))
1704
+ .onSecondCall()
1705
+ .resolves(createMockBridgeQuote({
1706
+ fromAmount: sourceInventory + 1n,
1707
+ toAmount: targetWithBuffer,
1708
+ toAmountMin: targetWithBuffer,
1709
+ requestParams: {
1710
+ fromChain: 42161,
1711
+ toChain: 1399811149,
1712
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1713
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1714
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1715
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1716
+ toAmount: targetWithBuffer,
1717
+ },
1718
+ }))
1719
+ .onThirdCall()
1720
+ .resolves(createMockBridgeQuote({
1721
+ fromAmount: sourceInventory + 1n,
1722
+ toAmount: targetWithBuffer - 1n,
1723
+ toAmountMin: targetWithBuffer - 1n,
1724
+ requestParams: {
1725
+ fromChain: 42161,
1726
+ toChain: 1399811149,
1727
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1728
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1729
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1730
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1731
+ fromAmount: sourceInventory,
1732
+ },
1733
+ }));
1734
+ const results = await inventoryRebalancer.rebalance([route]);
1735
+ expect(results).to.have.lengthOf(1);
1736
+ expect(results[0].success).to.be.false;
1737
+ expect(results[0].error).to.include('All inventory movements failed');
1738
+ expect(results[0].error).to.include('exceeded planned source capacity');
1739
+ expect(bridge.quote.callCount).to.equal(3);
1740
+ expect(bridge.execute.called).to.be.false;
1741
+ expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
1742
+ expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(sourceInventory);
1743
+ expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
1744
+ });
1745
+ it('fails gracefully when execute-time bridge quote rejects', async () => {
1746
+ const amount = BigInt(1e18);
1747
+ const sourceInventory = BigInt(2e18);
1748
+ const route = createTestRoute({ amount });
1749
+ createTestIntent({ amount });
1750
+ inventoryRebalancer.setInventoryBalances({
1751
+ [SOLANA_CHAIN]: 0n,
1752
+ [ARBITRUM_CHAIN]: sourceInventory,
1753
+ });
1754
+ bridge.quote
1755
+ .onFirstCall()
1756
+ .resolves(createMockBridgeQuote({
1757
+ fromAmount: sourceInventory,
1758
+ toAmount: sourceInventory,
1759
+ toAmountMin: sourceInventory,
1760
+ requestParams: {
1761
+ fromChain: 42161,
1762
+ toChain: 1399811149,
1763
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1764
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1765
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1766
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1767
+ fromAmount: sourceInventory,
1768
+ },
1769
+ }))
1770
+ .onSecondCall()
1771
+ .rejects(new Error('LiFi API timeout'));
1772
+ const results = await inventoryRebalancer.rebalance([route]);
1773
+ expect(results).to.have.lengthOf(1);
1774
+ expect(results[0].success).to.be.false;
1775
+ expect(results[0].error).to.include('All inventory movements failed');
1776
+ expect(results[0].error).to.include('LiFi API timeout');
1777
+ expect(bridge.quote.callCount).to.equal(2);
1778
+ expect(bridge.execute.called).to.be.false;
1779
+ });
1780
+ it('preserves non-Error rejection reasons from parallel bridge execution', async () => {
1781
+ const amount = BigInt(1e18);
1782
+ const sourceInventory = BigInt(2e18);
1783
+ const route = createTestRoute({ amount });
1784
+ createTestIntent({ amount });
1785
+ inventoryRebalancer.setInventoryBalances({
1786
+ [SOLANA_CHAIN]: 0n,
1787
+ [ARBITRUM_CHAIN]: sourceInventory,
1788
+ });
1789
+ bridge.quote.onFirstCall().resolves(createMockBridgeQuote({
1790
+ fromAmount: sourceInventory,
1791
+ toAmount: sourceInventory,
1792
+ toAmountMin: sourceInventory,
1793
+ requestParams: {
1794
+ fromChain: 42161,
1795
+ toChain: 1399811149,
1796
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1797
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
1798
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1799
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1800
+ fromAmount: sourceInventory,
1801
+ },
1802
+ }));
1803
+ // Simulate a foreign promise rejecting with a non-Error reason at runtime.
1804
+ const rejectionReason = 'bridge exploded';
1805
+ const executeInventoryMovementStub = Sinon.stub(inventoryRebalancer, 'executeInventoryMovement').callsFake(() => Promise.resolve().then(() => {
1806
+ throw rejectionReason;
1807
+ }));
1808
+ const results = await inventoryRebalancer.rebalance([route]);
1809
+ expect(results).to.have.lengthOf(1);
1810
+ expect(results[0].success).to.be.false;
1811
+ expect(results[0].error).to.include('All inventory movements failed');
1812
+ expect(results[0].error).to.include('arbitrum: bridge exploded');
1813
+ expect(executeInventoryMovementStub.calledOnce).to.be.true;
1814
+ expect(bridge.execute.called).to.be.false;
1815
+ executeInventoryMovementStub.restore();
1816
+ });
1817
+ it('logs actual successful bridge output floors instead of planned output totals', async () => {
1818
+ const amount = BigInt(1e18);
1819
+ const perChainInventory = BigInt(0.6e18);
1820
+ const logger = {
1821
+ info: Sinon.stub(),
1822
+ debug: Sinon.stub(),
1823
+ warn: Sinon.stub(),
1824
+ error: Sinon.stub(),
1825
+ };
1826
+ const localRebalancer = new InventoryRebalancer(config, actionTracker, { lifi: bridge }, warpCore, multiProvider, logger);
1827
+ const route = createTestRoute({ amount });
1828
+ createTestIntent({ amount });
1829
+ localRebalancer.setInventoryBalances({
1830
+ [SOLANA_CHAIN]: 0n,
1831
+ [ARBITRUM_CHAIN]: perChainInventory,
1832
+ [BASE_CHAIN]: perChainInventory,
1833
+ });
1834
+ bridge.quote
1835
+ .onCall(0)
1836
+ .resolves(createMockBridgeQuote({
1837
+ fromAmount: perChainInventory,
1838
+ toAmount: BigInt(0.525e18),
1839
+ toAmountMin: BigInt(0.525e18),
1840
+ }))
1841
+ .onCall(1)
1842
+ .resolves(createMockBridgeQuote({
1843
+ fromAmount: perChainInventory,
1844
+ toAmount: BigInt(0.525e18),
1845
+ toAmountMin: BigInt(0.525e18),
1846
+ }))
1847
+ .onCall(2)
1848
+ .resolves(createMockBridgeQuote({
1849
+ fromAmount: perChainInventory,
1850
+ toAmount: BigInt(0.505e18),
1851
+ toAmountMin: BigInt(0.5e18),
1852
+ }))
1853
+ .onCall(3)
1854
+ .resolves(createMockBridgeQuote({
1855
+ fromAmount: perChainInventory,
1856
+ toAmount: BigInt(0.515e18),
1857
+ toAmountMin: BigInt(0.51e18),
1858
+ }));
1859
+ bridge.execute
1860
+ .onFirstCall()
1861
+ .resolves({
1862
+ txHash: '0xSuccessTxHash1',
1863
+ fromChain: 42161,
1864
+ toChain: 1399811149,
1865
+ })
1866
+ .onSecondCall()
1867
+ .resolves({
1868
+ txHash: '0xSuccessTxHash2',
1869
+ fromChain: 8453,
1870
+ toChain: 1399811149,
1871
+ });
1872
+ const results = await localRebalancer.rebalance([route]);
1873
+ expect(results).to.have.lengthOf(1);
1874
+ expect(results[0].success).to.be.true;
1875
+ expect(bridge.execute.callCount).to.equal(2);
1876
+ const summaryCall = logger.info
1877
+ .getCalls()
1878
+ .find((call) => call.args[0] &&
1879
+ typeof call.args[0] === 'object' &&
1880
+ Object.prototype.hasOwnProperty.call(call.args[0], 'totalQuotedOutputMin'));
1881
+ assert(summaryCall, 'Expected summary log with totalQuotedOutputMin');
1882
+ expect(summaryCall.args[0].totalQuotedOutputMin).to.equal(BigInt(1.01e18).toString());
1225
1883
  });
1226
1884
  it('continues when some bridges fail', async () => {
1227
1885
  const amount = BigInt(1e18);
@@ -1402,11 +2060,31 @@ describe('InventoryRebalancer E2E', () => {
1402
2060
  [ARBITRUM_CHAIN]: tokenBalance,
1403
2061
  });
1404
2062
  // Quote with high gas costs (but for ERC20, this shouldn't trigger viability check)
1405
- bridge.quote.resolves(createMockBridgeQuote({
1406
- fromAmount: BigInt(1.05e18),
1407
- toAmount: BigInt(1e18),
2063
+ bridge.quote
2064
+ .onFirstCall()
2065
+ .resolves(createMockBridgeQuote({
2066
+ fromAmount: tokenBalance,
2067
+ toAmount: tokenBalance,
2068
+ toAmountMin: tokenBalance,
1408
2069
  gasCosts: BigInt(1e18), // Very high gas (would fail viability if this were native)
1409
2070
  feeCosts: 0n,
2071
+ }))
2072
+ .onSecondCall()
2073
+ .resolves(createMockBridgeQuote({
2074
+ fromAmount: tokenBalance,
2075
+ toAmount: tokenBalance,
2076
+ toAmountMin: tokenBalance,
2077
+ gasCosts: BigInt(1e18),
2078
+ feeCosts: 0n,
2079
+ requestParams: {
2080
+ fromChain: 42161,
2081
+ toChain: 1399811149,
2082
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
2083
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
2084
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
2085
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
2086
+ toAmount: tokenBalance,
2087
+ },
1410
2088
  }));
1411
2089
  bridge.execute.resolves({
1412
2090
  txHash: '0xERC20BridgeTxHash',
@@ -1418,6 +2096,9 @@ describe('InventoryRebalancer E2E', () => {
1418
2096
  expect(results).to.have.lengthOf(1);
1419
2097
  expect(results[0].success).to.be.true;
1420
2098
  expect(bridge.execute.calledOnce).to.be.true;
2099
+ expect(bridge.quote.callCount).to.equal(2);
2100
+ expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(tokenBalance);
2101
+ expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
1421
2102
  });
1422
2103
  it('calculates max viable amount as inventory minus (gasCosts * 20) for native tokens', async () => {
1423
2104
  // Setup: Native token with enough balance for viable bridge
@@ -1447,11 +2128,40 @@ describe('InventoryRebalancer E2E', () => {
1447
2128
  // maxViable = 1 ETH - 0.02 ETH = 0.98 ETH
1448
2129
  // 10% threshold = 0.1 ETH, estimatedGas (0.02) < threshold → viable
1449
2130
  const gasCosts = BigInt(0.001e18); // 0.001 ETH
1450
- bridge.quote.resolves(createMockBridgeQuote({
2131
+ const maxViable = rawBalance - gasCosts * 20n;
2132
+ bridge.quote
2133
+ .onFirstCall()
2134
+ .resolves(createMockBridgeQuote({
1451
2135
  fromAmount: rawBalance,
1452
2136
  toAmount: rawBalance - BigInt(1e15),
2137
+ toAmountMin: rawBalance - BigInt(1e15),
1453
2138
  gasCosts,
1454
2139
  feeCosts: 0n,
2140
+ }))
2141
+ .onSecondCall()
2142
+ .resolves(createMockBridgeQuote({
2143
+ fromAmount: maxViable,
2144
+ toAmount: maxViable - BigInt(1e15),
2145
+ toAmountMin: maxViable - BigInt(1e15),
2146
+ gasCosts,
2147
+ feeCosts: 0n,
2148
+ }))
2149
+ .onThirdCall()
2150
+ .resolves(createMockBridgeQuote({
2151
+ fromAmount: BigInt(0.525e18),
2152
+ toAmount: BigInt(0.525e18),
2153
+ toAmountMin: BigInt(0.525e18),
2154
+ gasCosts,
2155
+ feeCosts: 0n,
2156
+ requestParams: {
2157
+ fromChain: 42161,
2158
+ toChain: 1399811149,
2159
+ fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
2160
+ toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
2161
+ fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
2162
+ toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
2163
+ toAmount: BigInt(0.525e18),
2164
+ },
1455
2165
  }));
1456
2166
  bridge.execute.resolves({
1457
2167
  txHash: '0xSuccessBridgeTxHash',
@@ -1463,15 +2173,94 @@ describe('InventoryRebalancer E2E', () => {
1463
2173
  expect(results).to.have.lengthOf(1);
1464
2174
  expect(results[0].success).to.be.true;
1465
2175
  expect(bridge.execute.calledOnce).to.be.true;
1466
- // Verify: The quoted fromAmount should be the target (since maxViable > target)
1467
- // For the execution quote (second quote call):
1468
- // targetWithBuffer = (0.5 ETH) * 1.05 = 0.525 ETH (for non-inventory execution, costs are 0)
1469
- const executionQuoteCall = bridge.quote
1470
- .getCalls()
1471
- .find((call) => call.args[0].fromAmount !== undefined &&
1472
- call.args[0].fromAmount !== rawBalance);
1473
- // Since maxViable (0.98 ETH) > targetWithBuffer (0.525 ETH), we bridge exactly targetWithBuffer
1474
- expect(executionQuoteCall).to.exist;
2176
+ expect(bridge.quote.callCount).to.equal(3);
2177
+ expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(maxViable);
2178
+ const executionTargetOutput = bridge.quote.getCall(2).args[0].toAmount;
2179
+ expect(executionTargetOutput).to.be.a('bigint');
2180
+ if (executionTargetOutput === undefined) {
2181
+ throw new Error('Expected reverse quote to set toAmount');
2182
+ }
2183
+ expect(executionTargetOutput > amount).to.be.true;
2184
+ expect(executionTargetOutput <= maxViable).to.be.true;
2185
+ });
2186
+ it('uses forward quote when target output exactly matches source capacity', async () => {
2187
+ const amount = 1000n;
2188
+ const rawBalance = 1100n;
2189
+ const targetWithBuffer = 1050n;
2190
+ const route = createTestRoute({ amount });
2191
+ createTestIntent({ amount });
2192
+ inventoryRebalancer.setInventoryBalances({
2193
+ [SOLANA_CHAIN]: 0n,
2194
+ [ARBITRUM_CHAIN]: rawBalance,
2195
+ });
2196
+ bridge.quote
2197
+ .onFirstCall()
2198
+ .resolves(createMockBridgeQuote({
2199
+ fromAmount: rawBalance,
2200
+ toAmount: targetWithBuffer,
2201
+ toAmountMin: targetWithBuffer,
2202
+ }))
2203
+ .onSecondCall()
2204
+ .resolves(createMockBridgeQuote({
2205
+ fromAmount: rawBalance,
2206
+ toAmount: targetWithBuffer,
2207
+ toAmountMin: targetWithBuffer,
2208
+ }));
2209
+ bridge.execute.resolves({
2210
+ txHash: '0xForwardBoundaryBridgeTxHash',
2211
+ fromChain: 42161,
2212
+ toChain: 1399811149,
2213
+ });
2214
+ const results = await inventoryRebalancer.rebalance([route]);
2215
+ expect(results).to.have.lengthOf(1);
2216
+ expect(results[0].success).to.be.true;
2217
+ expect(bridge.execute.calledOnce).to.be.true;
2218
+ expect(bridge.quote.callCount).to.equal(2);
2219
+ expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(rawBalance);
2220
+ expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
2221
+ });
2222
+ it('retries reverse quote as forward when exact-output exceeds source capacity', async () => {
2223
+ const amount = 1000n;
2224
+ const rawBalance = 1100n;
2225
+ const targetWithBuffer = 1050n;
2226
+ const route = createTestRoute({ amount });
2227
+ createTestIntent({ amount });
2228
+ inventoryRebalancer.setInventoryBalances({
2229
+ [SOLANA_CHAIN]: 0n,
2230
+ [ARBITRUM_CHAIN]: rawBalance,
2231
+ });
2232
+ bridge.quote
2233
+ .onFirstCall()
2234
+ .resolves(createMockBridgeQuote({
2235
+ fromAmount: rawBalance,
2236
+ toAmount: 1051n,
2237
+ toAmountMin: 1051n,
2238
+ }))
2239
+ .onSecondCall()
2240
+ .resolves(createMockBridgeQuote({
2241
+ fromAmount: rawBalance + 1n,
2242
+ toAmount: targetWithBuffer,
2243
+ toAmountMin: targetWithBuffer,
2244
+ }))
2245
+ .onThirdCall()
2246
+ .resolves(createMockBridgeQuote({
2247
+ fromAmount: rawBalance,
2248
+ toAmount: 1049n,
2249
+ toAmountMin: 1049n,
2250
+ }));
2251
+ bridge.execute.resolves({
2252
+ txHash: '0xForwardFallbackBridgeTxHash',
2253
+ fromChain: 42161,
2254
+ toChain: 1399811149,
2255
+ });
2256
+ const results = await inventoryRebalancer.rebalance([route]);
2257
+ expect(results).to.have.lengthOf(1);
2258
+ expect(results[0].success).to.be.true;
2259
+ expect(bridge.execute.calledOnce).to.be.true;
2260
+ expect(bridge.quote.callCount).to.equal(3);
2261
+ expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
2262
+ expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(rawBalance);
2263
+ expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
1475
2264
  });
1476
2265
  it('handles quote failures gracefully by skipping the source chain', async () => {
1477
2266
  // Setup: Native token where quote fails