@hyperlane-xyz/rebalancer 2.0.0 → 3.0.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 (209) hide show
  1. package/dist/bridges/LiFiBridge.d.ts +67 -0
  2. package/dist/bridges/LiFiBridge.d.ts.map +1 -0
  3. package/dist/bridges/LiFiBridge.js +386 -0
  4. package/dist/bridges/LiFiBridge.js.map +1 -0
  5. package/dist/config/RebalancerConfig.d.ts +7 -2
  6. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  7. package/dist/config/RebalancerConfig.js +7 -4
  8. package/dist/config/RebalancerConfig.js.map +1 -1
  9. package/dist/config/RebalancerConfig.test.js +134 -1
  10. package/dist/config/RebalancerConfig.test.js.map +1 -1
  11. package/dist/config/types.d.ts +1016 -304
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +105 -10
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/core/InventoryRebalancer.d.ts +190 -0
  16. package/dist/core/InventoryRebalancer.d.ts.map +1 -0
  17. package/dist/core/InventoryRebalancer.js +885 -0
  18. package/dist/core/InventoryRebalancer.js.map +1 -0
  19. package/dist/core/InventoryRebalancer.test.d.ts +2 -0
  20. package/dist/core/InventoryRebalancer.test.d.ts.map +1 -0
  21. package/dist/core/InventoryRebalancer.test.js +1351 -0
  22. package/dist/core/InventoryRebalancer.test.js.map +1 -0
  23. package/dist/core/Rebalancer.d.ts +11 -4
  24. package/dist/core/Rebalancer.d.ts.map +1 -1
  25. package/dist/core/Rebalancer.js +92 -9
  26. package/dist/core/Rebalancer.js.map +1 -1
  27. package/dist/core/Rebalancer.test.js +82 -49
  28. package/dist/core/Rebalancer.test.js.map +1 -1
  29. package/dist/core/RebalancerOrchestrator.d.ts +30 -9
  30. package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
  31. package/dist/core/RebalancerOrchestrator.js +79 -71
  32. package/dist/core/RebalancerOrchestrator.js.map +1 -1
  33. package/dist/core/RebalancerOrchestrator.test.d.ts +2 -0
  34. package/dist/core/RebalancerOrchestrator.test.d.ts.map +1 -0
  35. package/dist/core/RebalancerOrchestrator.test.js +714 -0
  36. package/dist/core/RebalancerOrchestrator.test.js.map +1 -0
  37. package/dist/core/RebalancerService.d.ts +7 -3
  38. package/dist/core/RebalancerService.d.ts.map +1 -1
  39. package/dist/core/RebalancerService.js +44 -24
  40. package/dist/core/RebalancerService.js.map +1 -1
  41. package/dist/core/RebalancerService.test.js +71 -109
  42. package/dist/core/RebalancerService.test.js.map +1 -1
  43. package/dist/e2e/collateral-deficit.e2e-test.js +1 -3
  44. package/dist/e2e/collateral-deficit.e2e-test.js.map +1 -1
  45. package/dist/e2e/composite.e2e-test.js.map +1 -1
  46. package/dist/e2e/harness/BridgeSetup.d.ts +6 -0
  47. package/dist/e2e/harness/BridgeSetup.d.ts.map +1 -1
  48. package/dist/e2e/harness/BridgeSetup.js +10 -1
  49. package/dist/e2e/harness/BridgeSetup.js.map +1 -1
  50. package/dist/e2e/harness/TestHelpers.d.ts.map +1 -1
  51. package/dist/e2e/harness/TestHelpers.js +1 -4
  52. package/dist/e2e/harness/TestHelpers.js.map +1 -1
  53. package/dist/e2e/harness/TestRebalancer.d.ts +1 -1
  54. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  55. package/dist/e2e/harness/TestRebalancer.js +6 -7
  56. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  57. package/dist/e2e/minAmount.e2e-test.js +0 -1
  58. package/dist/e2e/minAmount.e2e-test.js.map +1 -1
  59. package/dist/e2e/weighted.e2e-test.js +0 -1
  60. package/dist/e2e/weighted.e2e-test.js.map +1 -1
  61. package/dist/factories/RebalancerContextFactory.d.ts +48 -6
  62. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  63. package/dist/factories/RebalancerContextFactory.js +170 -17
  64. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  65. package/dist/index.d.ts +5 -5
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/interfaces/IExternalBridge.d.ts +101 -0
  70. package/dist/interfaces/IExternalBridge.d.ts.map +1 -0
  71. package/dist/interfaces/IExternalBridge.js +2 -0
  72. package/dist/interfaces/IExternalBridge.js.map +1 -0
  73. package/dist/interfaces/IMonitor.d.ts +1 -0
  74. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  75. package/dist/interfaces/IRebalancer.d.ts +25 -25
  76. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  77. package/dist/interfaces/IStrategy.d.ts +36 -3
  78. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  79. package/dist/interfaces/IStrategy.js +12 -1
  80. package/dist/interfaces/IStrategy.js.map +1 -1
  81. package/dist/metrics/PriceGetter.js +1 -1
  82. package/dist/metrics/PriceGetter.js.map +1 -1
  83. package/dist/metrics/scripts/metrics.d.ts +3 -3
  84. package/dist/monitor/Monitor.d.ts +12 -2
  85. package/dist/monitor/Monitor.d.ts.map +1 -1
  86. package/dist/monitor/Monitor.js +46 -1
  87. package/dist/monitor/Monitor.js.map +1 -1
  88. package/dist/service.js +40 -17
  89. package/dist/service.js.map +1 -1
  90. package/dist/strategy/BaseStrategy.d.ts +12 -6
  91. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  92. package/dist/strategy/BaseStrategy.js +56 -21
  93. package/dist/strategy/BaseStrategy.js.map +1 -1
  94. package/dist/strategy/CollateralDeficitStrategy.d.ts +1 -1
  95. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
  96. package/dist/strategy/CollateralDeficitStrategy.js +19 -11
  97. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
  98. package/dist/strategy/CollateralDeficitStrategy.test.js +135 -2
  99. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -1
  100. package/dist/strategy/CompositeStrategy.test.js +13 -0
  101. package/dist/strategy/CompositeStrategy.test.js.map +1 -1
  102. package/dist/strategy/MinAmountStrategy.test.js +4 -0
  103. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  104. package/dist/strategy/StrategyFactory.d.ts +2 -1
  105. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  106. package/dist/strategy/StrategyFactory.js +24 -8
  107. package/dist/strategy/StrategyFactory.js.map +1 -1
  108. package/dist/strategy/WeightedStrategy.test.js +6 -0
  109. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  110. package/dist/test/helpers.d.ts +8 -7
  111. package/dist/test/helpers.d.ts.map +1 -1
  112. package/dist/test/helpers.js +23 -5
  113. package/dist/test/helpers.js.map +1 -1
  114. package/dist/test/lifiMocks.d.ts +51 -0
  115. package/dist/test/lifiMocks.d.ts.map +1 -0
  116. package/dist/test/lifiMocks.js +130 -0
  117. package/dist/test/lifiMocks.js.map +1 -0
  118. package/dist/tracking/ActionTracker.d.ts +33 -1
  119. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  120. package/dist/tracking/ActionTracker.js +193 -22
  121. package/dist/tracking/ActionTracker.js.map +1 -1
  122. package/dist/tracking/ActionTracker.test.js +107 -19
  123. package/dist/tracking/ActionTracker.test.js.map +1 -1
  124. package/dist/tracking/IActionTracker.d.ts +47 -3
  125. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  126. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
  127. package/dist/tracking/InflightContextAdapter.js +24 -7
  128. package/dist/tracking/InflightContextAdapter.js.map +1 -1
  129. package/dist/tracking/InflightContextAdapter.test.js +7 -4
  130. package/dist/tracking/InflightContextAdapter.test.js.map +1 -1
  131. package/dist/tracking/types.d.ts +31 -2
  132. package/dist/tracking/types.d.ts.map +1 -1
  133. package/dist/utils/ExplorerClient.d.ts +2 -1
  134. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  135. package/dist/utils/ExplorerClient.js +13 -8
  136. package/dist/utils/ExplorerClient.js.map +1 -1
  137. package/dist/utils/bridgeUtils.d.ts +27 -4
  138. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  139. package/dist/utils/bridgeUtils.js +38 -0
  140. package/dist/utils/bridgeUtils.js.map +1 -1
  141. package/dist/utils/bridgeUtils.test.js +9 -0
  142. package/dist/utils/bridgeUtils.test.js.map +1 -1
  143. package/dist/utils/gasEstimation.d.ts +65 -0
  144. package/dist/utils/gasEstimation.d.ts.map +1 -0
  145. package/dist/utils/gasEstimation.js +176 -0
  146. package/dist/utils/gasEstimation.js.map +1 -0
  147. package/dist/utils/tokenUtils.d.ts +9 -1
  148. package/dist/utils/tokenUtils.d.ts.map +1 -1
  149. package/dist/utils/tokenUtils.js +11 -0
  150. package/dist/utils/tokenUtils.js.map +1 -1
  151. package/package.json +9 -7
  152. package/src/bridges/LiFiBridge.ts +538 -0
  153. package/src/config/RebalancerConfig.test.ts +160 -0
  154. package/src/config/RebalancerConfig.ts +14 -3
  155. package/src/config/types.ts +136 -10
  156. package/src/core/InventoryRebalancer.test.ts +1684 -0
  157. package/src/core/InventoryRebalancer.ts +1255 -0
  158. package/src/core/Rebalancer.test.ts +84 -30
  159. package/src/core/Rebalancer.ts +144 -23
  160. package/src/core/RebalancerOrchestrator.test.ts +860 -0
  161. package/src/core/RebalancerOrchestrator.ts +146 -95
  162. package/src/core/RebalancerService.test.ts +80 -123
  163. package/src/core/RebalancerService.ts +67 -33
  164. package/src/e2e/collateral-deficit.e2e-test.ts +2 -4
  165. package/src/e2e/composite.e2e-test.ts +5 -5
  166. package/src/e2e/harness/BridgeSetup.ts +28 -1
  167. package/src/e2e/harness/TestHelpers.ts +1 -4
  168. package/src/e2e/harness/TestRebalancer.ts +7 -7
  169. package/src/e2e/minAmount.e2e-test.ts +1 -2
  170. package/src/e2e/weighted.e2e-test.ts +1 -2
  171. package/src/factories/RebalancerContextFactory.ts +293 -24
  172. package/src/index.ts +20 -5
  173. package/src/interfaces/IExternalBridge.ts +115 -0
  174. package/src/interfaces/IMonitor.ts +1 -0
  175. package/src/interfaces/IRebalancer.ts +45 -29
  176. package/src/interfaces/IStrategy.ts +50 -3
  177. package/src/metrics/PriceGetter.ts +1 -1
  178. package/src/monitor/Monitor.ts +81 -2
  179. package/src/service.ts +59 -18
  180. package/src/strategy/BaseStrategy.ts +77 -24
  181. package/src/strategy/CollateralDeficitStrategy.test.ts +181 -4
  182. package/src/strategy/CollateralDeficitStrategy.ts +42 -15
  183. package/src/strategy/CompositeStrategy.test.ts +13 -0
  184. package/src/strategy/MinAmountStrategy.test.ts +4 -0
  185. package/src/strategy/StrategyFactory.ts +33 -6
  186. package/src/strategy/WeightedStrategy.test.ts +6 -0
  187. package/src/test/helpers.ts +39 -14
  188. package/src/test/lifiMocks.ts +174 -0
  189. package/src/tracking/ActionTracker.test.ts +122 -19
  190. package/src/tracking/ActionTracker.ts +284 -24
  191. package/src/tracking/IActionTracker.ts +58 -3
  192. package/src/tracking/InflightContextAdapter.test.ts +7 -4
  193. package/src/tracking/InflightContextAdapter.ts +42 -9
  194. package/src/tracking/types.ts +43 -2
  195. package/src/utils/ExplorerClient.ts +23 -10
  196. package/src/utils/bridgeUtils.test.ts +9 -0
  197. package/src/utils/bridgeUtils.ts +75 -6
  198. package/src/utils/gasEstimation.ts +272 -0
  199. package/src/utils/tokenUtils.ts +12 -0
  200. package/dist/tracking/index.d.ts +0 -7
  201. package/dist/tracking/index.d.ts.map +0 -1
  202. package/dist/tracking/index.js +0 -6
  203. package/dist/tracking/index.js.map +0 -1
  204. package/dist/utils/index.d.ts +0 -5
  205. package/dist/utils/index.d.ts.map +0 -1
  206. package/dist/utils/index.js +0 -5
  207. package/dist/utils/index.js.map +0 -1
  208. package/src/tracking/index.ts +0 -36
  209. package/src/utils/index.ts +0 -4
@@ -8,14 +8,15 @@ import type { MultiProvider, Token, WarpCore } from '@hyperlane-xyz/sdk';
8
8
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
9
9
  import { RebalancerStrategyOptions } from '../config/types.js';
10
10
  import { RebalancerContextFactory } from '../factories/RebalancerContextFactory.js';
11
+ import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
11
12
  import { MonitorEventType } from '../interfaces/IMonitor.js';
12
13
  import type { IRebalancer } from '../interfaces/IRebalancer.js';
13
14
  import type { IStrategy } from '../interfaces/IStrategy.js';
14
15
  import { Metrics } from '../metrics/Metrics.js';
15
16
  import { Monitor } from '../monitor/Monitor.js';
16
17
  import { TEST_ADDRESSES, getTestAddress } from '../test/helpers.js';
17
- import type { IActionTracker } from '../tracking/index.js';
18
- import { InflightContextAdapter } from '../tracking/index.js';
18
+ import type { IActionTracker } from '../tracking/IActionTracker.js';
19
+ import { InflightContextAdapter } from '../tracking/InflightContextAdapter.js';
19
20
 
20
21
  import {
21
22
  RebalancerService,
@@ -85,6 +86,7 @@ function createMockWarpCore(): WarpCore {
85
86
 
86
87
  function createMockRebalancer(): IRebalancer & { rebalance: Sinon.SinonStub } {
87
88
  return {
89
+ rebalancerType: 'movableCollateral' as const,
88
90
  rebalance: Sinon.stub().resolves([]),
89
91
  };
90
92
  }
@@ -114,6 +116,10 @@ function createMockActionTracker(): IActionTracker {
114
116
  syncTransfers: Sinon.stub().resolves(),
115
117
  syncRebalanceIntents: Sinon.stub().resolves(),
116
118
  syncRebalanceActions: Sinon.stub().resolves(),
119
+ syncInventoryMovementActions: Sinon.stub().resolves({
120
+ completed: 0,
121
+ failed: 0,
122
+ }),
117
123
  logStoreContents: Sinon.stub().resolves(),
118
124
  getInProgressTransfers: Sinon.stub().resolves([]),
119
125
  getActiveRebalanceIntents: Sinon.stub().resolves([]),
@@ -123,6 +129,10 @@ function createMockActionTracker(): IActionTracker {
123
129
  getRebalanceIntent: Sinon.stub().resolves(undefined),
124
130
  getRebalanceAction: Sinon.stub().resolves(undefined),
125
131
  getInProgressActions: Sinon.stub().resolves([]),
132
+ getPartiallyFulfilledInventoryIntents: Sinon.stub().resolves([]),
133
+ getActionsByType: Sinon.stub().resolves([]),
134
+ getActionsForIntent: Sinon.stub().resolves([]),
135
+ getInflightInventoryMovements: Sinon.stub().resolves(0n),
126
136
  };
127
137
  }
128
138
 
@@ -168,7 +178,12 @@ function createMockContextFactory(
168
178
  getWarpCore: () => warpCore,
169
179
  getTokenForChain: (chain: string) =>
170
180
  warpCore.tokens.find((t) => t.chainName === chain),
171
- createRebalancer: () => rebalancer,
181
+ createRebalancer: (_actionTracker: IActionTracker) => rebalancer,
182
+ createRebalancers: async (_actionTracker: IActionTracker) => ({
183
+ rebalancers: [rebalancer],
184
+ externalBridgeRegistry: {},
185
+ inventoryConfig: undefined,
186
+ }),
172
187
  createStrategy: async () => strategy,
173
188
  createMonitor: () => monitor,
174
189
  createMetrics: async () => overrides.metrics ?? ({} as Metrics),
@@ -176,6 +191,33 @@ function createMockContextFactory(
176
191
  tracker: actionTracker,
177
192
  adapter: inflightAdapter,
178
193
  }),
194
+ createOrchestrator: (options: {
195
+ strategy: IStrategy;
196
+ actionTracker: IActionTracker;
197
+ inflightContextAdapter: InflightContextAdapter;
198
+ rebalancers: IRebalancer[];
199
+ externalBridgeRegistry: Partial<ExternalBridgeRegistry>;
200
+ metrics?: Metrics;
201
+ }) => ({
202
+ executeCycle: Sinon.stub().callsFake(async (_event: any) => {
203
+ // Simulate orchestrator behavior: call strategy, then rebalancer, then record metrics
204
+ const strategyWithGetRoutes = options.strategy as any;
205
+ const routes = strategyWithGetRoutes.getRebalancingRoutes?.() ?? [];
206
+ if (routes.length > 0 && options.rebalancers[0]) {
207
+ const results = await options.rebalancers[0].rebalance(routes);
208
+ if (options.metrics && results) {
209
+ results.forEach((result: any) => {
210
+ if (result.success) {
211
+ (options.metrics as any).recordRebalancerSuccess?.();
212
+ } else {
213
+ (options.metrics as any).recordRebalancerFailure?.();
214
+ }
215
+ });
216
+ }
217
+ }
218
+ return { success: true };
219
+ }),
220
+ }),
179
221
  } as unknown as RebalancerContextFactory;
180
222
  }
181
223
 
@@ -189,13 +231,11 @@ interface DaemonTestSetup {
189
231
  async function setupDaemonTest(
190
232
  sandbox: Sinon.SinonSandbox,
191
233
  options: {
192
- intentIds?: string[];
193
234
  rebalanceResults: Array<{
194
235
  route: {
195
236
  origin: string;
196
237
  destination: string;
197
238
  amount: bigint;
198
- intentId: string;
199
239
  bridge: string;
200
240
  };
201
241
  success: boolean;
@@ -212,14 +252,6 @@ async function setupDaemonTest(
212
252
  },
213
253
  ): Promise<DaemonTestSetup> {
214
254
  const actionTracker = createMockActionTracker();
215
- let intentIndex = 0;
216
- (actionTracker.createRebalanceIntent as Sinon.SinonStub).callsFake(
217
- async () => ({
218
- id: options.intentIds?.[intentIndex] ?? `intent-${intentIndex + 1}`,
219
- status: 'not_started' as const,
220
- ...(intentIndex++, {}),
221
- }),
222
- );
223
255
 
224
256
  const rebalancer = createMockRebalancer();
225
257
  rebalancer.rebalance.resolves(options.rebalanceResults);
@@ -253,6 +285,7 @@ async function setupDaemonTest(
253
285
  const service = new RebalancerService(
254
286
  createMockMultiProvider(),
255
287
  undefined,
288
+ undefined,
256
289
  {} as any,
257
290
  createMockRebalancerConfig(),
258
291
  { mode: 'daemon', checkFrequency: 60000, logger: testLogger },
@@ -312,6 +345,7 @@ describe('RebalancerService', () => {
312
345
  const service = new RebalancerService(
313
346
  createMockMultiProvider(),
314
347
  undefined,
348
+ undefined,
315
349
  {} as any,
316
350
  createMockRebalancerConfig(),
317
351
  config,
@@ -347,6 +381,7 @@ describe('RebalancerService', () => {
347
381
  const service = new RebalancerService(
348
382
  createMockMultiProvider(),
349
383
  undefined,
384
+ undefined,
350
385
  {} as any,
351
386
  createMockRebalancerConfig(),
352
387
  config,
@@ -373,6 +408,7 @@ describe('RebalancerService', () => {
373
408
  const service = new RebalancerService(
374
409
  createMockMultiProvider(),
375
410
  undefined,
411
+ undefined,
376
412
  {} as any,
377
413
  createMockRebalancerConfig(),
378
414
  config,
@@ -399,6 +435,7 @@ describe('RebalancerService', () => {
399
435
  const service = new RebalancerService(
400
436
  createMockMultiProvider(),
401
437
  undefined,
438
+ undefined,
402
439
  {} as any,
403
440
  createMockRebalancerConfig(),
404
441
  config,
@@ -449,6 +486,7 @@ describe('RebalancerService', () => {
449
486
  const service = new RebalancerService(
450
487
  createMockMultiProvider(),
451
488
  undefined,
489
+ undefined,
452
490
  {} as any,
453
491
  configWithoutBridge,
454
492
  config,
@@ -476,6 +514,7 @@ describe('RebalancerService', () => {
476
514
  const service = new RebalancerService(
477
515
  createMockMultiProvider(),
478
516
  undefined,
517
+ undefined,
479
518
  {} as any,
480
519
  createMockRebalancerConfig(),
481
520
  config,
@@ -505,6 +544,7 @@ describe('RebalancerService', () => {
505
544
  const service = new RebalancerService(
506
545
  createMockMultiProvider(),
507
546
  undefined,
547
+ undefined,
508
548
  {} as any,
509
549
  createMockRebalancerConfig(),
510
550
  config,
@@ -530,6 +570,7 @@ describe('RebalancerService', () => {
530
570
  const service = new RebalancerService(
531
571
  createMockMultiProvider(),
532
572
  undefined,
573
+ undefined,
533
574
  {} as any,
534
575
  createMockRebalancerConfig(),
535
576
  config,
@@ -559,6 +600,7 @@ describe('RebalancerService', () => {
559
600
  const service = new RebalancerService(
560
601
  createMockMultiProvider(),
561
602
  undefined,
603
+ undefined,
562
604
  {} as any,
563
605
  createMockRebalancerConfig(),
564
606
  config,
@@ -591,6 +633,7 @@ describe('RebalancerService', () => {
591
633
  const service = new RebalancerService(
592
634
  createMockMultiProvider(),
593
635
  undefined,
636
+ undefined,
594
637
  {} as any,
595
638
  createMockRebalancerConfig(),
596
639
  config,
@@ -674,6 +717,7 @@ describe('RebalancerService', () => {
674
717
  const service = new RebalancerService(
675
718
  createMockMultiProvider(),
676
719
  undefined,
720
+ undefined,
677
721
  {} as any,
678
722
  createMockRebalancerConfig(),
679
723
  config,
@@ -766,6 +810,7 @@ describe('RebalancerService', () => {
766
810
  const service = new RebalancerService(
767
811
  createMockMultiProvider(),
768
812
  undefined,
813
+ undefined,
769
814
  {} as any,
770
815
  createMockRebalancerConfig(),
771
816
  config,
@@ -875,6 +920,7 @@ describe('RebalancerService', () => {
875
920
  const service = new RebalancerService(
876
921
  createMockMultiProvider(),
877
922
  undefined,
923
+ undefined,
878
924
  {} as any,
879
925
  createMockRebalancerConfig(),
880
926
  config,
@@ -891,65 +937,24 @@ describe('RebalancerService', () => {
891
937
  });
892
938
 
893
939
  expect(recordRebalancerFailure.calledOnce).to.be.true;
894
- expect(recordRebalancerSuccess.called).to.be.false;
940
+ expect(recordRebalancerSuccess.calledOnce).to.be.true;
895
941
  });
896
942
  });
897
943
 
898
- describe('daemon mode intent tracking', () => {
899
- it('should call failRebalanceIntent with correct intentId when route fails', async () => {
900
- const { actionTracker, triggerCycle } = await setupDaemonTest(sandbox, {
901
- intentIds: ['intent-123'],
902
- rebalanceResults: [
903
- {
904
- route: {
905
- origin: 'ethereum',
906
- destination: 'arbitrum',
907
- amount: 1000n,
908
- intentId: 'intent-123',
909
- bridge: TEST_ADDRESSES.bridge,
910
- },
911
- success: false,
912
- error: 'Gas estimation failed',
913
- },
914
- ],
915
- strategyRoutes: [
916
- {
917
- origin: 'ethereum',
918
- destination: 'arbitrum',
919
- amount: 1000n,
920
- bridge: TEST_ADDRESSES.bridge,
921
- },
922
- ],
923
- });
924
-
925
- await triggerCycle();
926
-
927
- expect((actionTracker.failRebalanceIntent as Sinon.SinonStub).calledOnce)
928
- .to.be.true;
929
- expect(
930
- (actionTracker.failRebalanceIntent as Sinon.SinonStub).calledWith(
931
- 'intent-123',
932
- ),
933
- ).to.be.true;
934
- });
935
-
936
- it('should call createRebalanceAction with correct intentId when route succeeds', async () => {
937
- const { actionTracker, triggerCycle } = await setupDaemonTest(sandbox, {
938
- intentIds: ['intent-456'],
944
+ describe('daemon mode rebalancer calls', () => {
945
+ it('should call rebalancer with routes from strategy', async () => {
946
+ const { rebalancer, triggerCycle } = await setupDaemonTest(sandbox, {
939
947
  rebalanceResults: [
940
948
  {
941
949
  route: {
942
950
  origin: 'ethereum',
943
951
  destination: 'arbitrum',
944
952
  amount: 1000n,
945
- intentId: 'intent-456',
946
953
  bridge: TEST_ADDRESSES.bridge,
947
954
  },
948
955
  success: true,
949
956
  messageId:
950
957
  '0x1111111111111111111111111111111111111111111111111111111111111111',
951
- txHash:
952
- '0x2222222222222222222222222222222222222222222222222222222222222222',
953
958
  },
954
959
  ],
955
960
  strategyRoutes: [
@@ -964,42 +969,33 @@ describe('RebalancerService', () => {
964
969
 
965
970
  await triggerCycle();
966
971
 
967
- expect(
968
- (actionTracker.createRebalanceAction as Sinon.SinonStub).calledOnce,
969
- ).to.be.true;
970
- const callArgs = (actionTracker.createRebalanceAction as Sinon.SinonStub)
971
- .firstCall.args[0];
972
- expect(callArgs.intentId).to.equal('intent-456');
972
+ expect(rebalancer.rebalance.calledOnce).to.be.true;
973
+ const routesPassedToRebalancer = rebalancer.rebalance.firstCall.args[0];
974
+ expect(routesPassedToRebalancer).to.have.lengthOf(1);
975
+ expect(routesPassedToRebalancer[0].origin).to.equal('ethereum');
976
+ expect(routesPassedToRebalancer[0].destination).to.equal('arbitrum');
973
977
  });
974
978
 
975
- it('should handle mixed success/failure results with correct intent mapping', async () => {
976
- const { actionTracker, triggerCycle } = await setupDaemonTest(sandbox, {
977
- intentIds: ['intent-1', 'intent-2'],
979
+ it('should call rebalancer with multiple routes', async () => {
980
+ const { rebalancer, triggerCycle } = await setupDaemonTest(sandbox, {
978
981
  rebalanceResults: [
979
982
  {
980
983
  route: {
981
984
  origin: 'ethereum',
982
985
  destination: 'arbitrum',
983
986
  amount: 1000n,
984
- intentId: 'intent-1',
985
987
  bridge: TEST_ADDRESSES.bridge,
986
988
  },
987
989
  success: true,
988
- messageId:
989
- '0x1111111111111111111111111111111111111111111111111111111111111111',
990
- txHash:
991
- '0x2222222222222222222222222222222222222222222222222222222222222222',
992
990
  },
993
991
  {
994
992
  route: {
995
993
  origin: 'arbitrum',
996
994
  destination: 'ethereum',
997
995
  amount: 500n,
998
- intentId: 'intent-2',
999
996
  bridge: TEST_ADDRESSES.bridge,
1000
997
  },
1001
- success: false,
1002
- error: 'Insufficient funds',
998
+ success: true,
1003
999
  },
1004
1000
  ],
1005
1001
  strategyRoutes: [
@@ -1020,61 +1016,20 @@ describe('RebalancerService', () => {
1020
1016
 
1021
1017
  await triggerCycle();
1022
1018
 
1023
- // Verify createRebalanceAction called for intent-1 (success)
1024
- expect(
1025
- (actionTracker.createRebalanceAction as Sinon.SinonStub).calledOnce,
1026
- ).to.be.true;
1027
- expect(
1028
- (actionTracker.createRebalanceAction as Sinon.SinonStub).firstCall
1029
- .args[0].intentId,
1030
- ).to.equal('intent-1');
1031
-
1032
- // Verify failRebalanceIntent called for intent-2 (failure)
1033
- expect((actionTracker.failRebalanceIntent as Sinon.SinonStub).calledOnce)
1034
- .to.be.true;
1035
- expect(
1036
- (actionTracker.failRebalanceIntent as Sinon.SinonStub).calledWith(
1037
- 'intent-2',
1038
- ),
1039
- ).to.be.true;
1019
+ expect(rebalancer.rebalance.calledOnce).to.be.true;
1020
+ const routesPassedToRebalancer = rebalancer.rebalance.firstCall.args[0];
1021
+ expect(routesPassedToRebalancer).to.have.lengthOf(2);
1040
1022
  });
1041
1023
 
1042
- it('should assign intentId from createRebalanceIntent to route before calling rebalancer', async () => {
1024
+ it('should not call rebalancer when no routes proposed', async () => {
1043
1025
  const { rebalancer, triggerCycle } = await setupDaemonTest(sandbox, {
1044
- intentIds: ['generated-intent-id'],
1045
- rebalanceResults: [
1046
- {
1047
- route: {
1048
- origin: 'ethereum',
1049
- destination: 'arbitrum',
1050
- amount: 1000n,
1051
- intentId: 'generated-intent-id',
1052
- bridge: TEST_ADDRESSES.bridge,
1053
- },
1054
- success: true,
1055
- messageId:
1056
- '0x1111111111111111111111111111111111111111111111111111111111111111',
1057
- },
1058
- ],
1059
- strategyRoutes: [
1060
- {
1061
- origin: 'ethereum',
1062
- destination: 'arbitrum',
1063
- amount: 1000n,
1064
- bridge: TEST_ADDRESSES.bridge,
1065
- },
1066
- ],
1026
+ rebalanceResults: [],
1027
+ strategyRoutes: [],
1067
1028
  });
1068
1029
 
1069
1030
  await triggerCycle();
1070
1031
 
1071
- // Verify rebalancer.rebalance was called with routes that have intentId
1072
- expect(rebalancer.rebalance.calledOnce).to.be.true;
1073
- const routesPassedToRebalancer = rebalancer.rebalance.firstCall.args[0];
1074
- expect(routesPassedToRebalancer).to.have.lengthOf(1);
1075
- expect(routesPassedToRebalancer[0].intentId).to.equal(
1076
- 'generated-intent-id',
1077
- );
1032
+ expect(rebalancer.rebalance.called).to.be.false;
1078
1033
  });
1079
1034
  });
1080
1035
 
@@ -1093,6 +1048,7 @@ describe('RebalancerService', () => {
1093
1048
  const service = new RebalancerService(
1094
1049
  createMockMultiProvider(),
1095
1050
  undefined,
1051
+ undefined,
1096
1052
  {} as any,
1097
1053
  createMockRebalancerConfig(),
1098
1054
  config,
@@ -1130,6 +1086,7 @@ describe('RebalancerService', () => {
1130
1086
  const service = new RebalancerService(
1131
1087
  createMockMultiProvider(),
1132
1088
  undefined,
1089
+ undefined,
1133
1090
  {} as any,
1134
1091
  createMockRebalancerConfig(),
1135
1092
  config,
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'crypto';
2
1
  import { Logger } from 'pino';
3
2
 
4
3
  import { IRegistry } from '@hyperlane-xyz/registry';
@@ -15,20 +14,24 @@ import {
15
14
  getStrategyChainNames,
16
15
  } from '../config/types.js';
17
16
  import { RebalancerContextFactory } from '../factories/RebalancerContextFactory.js';
17
+ import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
18
18
  import {
19
19
  MonitorEvent,
20
20
  MonitorEventType,
21
21
  MonitorPollingError,
22
22
  MonitorStartError,
23
23
  } from '../interfaces/IMonitor.js';
24
- import type { IRebalancer, RebalanceRoute } from '../interfaces/IRebalancer.js';
25
- import type { IStrategy } from '../interfaces/IStrategy.js';
24
+ import type { IRebalancer } from '../interfaces/IRebalancer.js';
25
+ import type {
26
+ IStrategy,
27
+ MovableCollateralRoute,
28
+ } from '../interfaces/IStrategy.js';
26
29
  import { Metrics } from '../metrics/Metrics.js';
27
- import { Monitor } from '../monitor/Monitor.js';
28
-
29
- import { RebalancerOrchestrator } from './RebalancerOrchestrator.js';
30
+ import { type InventoryMonitorConfig, Monitor } from '../monitor/Monitor.js';
31
+ import type { IActionTracker } from '../tracking/IActionTracker.js';
30
32
  import { InflightContextAdapter } from '../tracking/InflightContextAdapter.js';
31
- import { IActionTracker } from '../tracking/IActionTracker.js';
33
+
34
+ import type { RebalancerOrchestrator } from './RebalancerOrchestrator.js';
32
35
 
33
36
  export interface RebalancerServiceConfig {
34
37
  /** Execution mode: 'manual' for one-off execution, 'daemon' for continuous monitoring */
@@ -118,10 +121,10 @@ export class RebalancerService {
118
121
  private mode: 'manual' | 'daemon';
119
122
  private actionTracker?: IActionTracker;
120
123
  private inflightContextAdapter?: InflightContextAdapter;
121
- private orchestrator!: RebalancerOrchestrator;
122
-
124
+ private orchestrator?: RebalancerOrchestrator;
123
125
  constructor(
124
126
  private readonly multiProvider: MultiProvider,
127
+ private readonly inventoryMultiProvider: MultiProvider | undefined,
125
128
  private readonly multiProtocolProvider: MultiProtocolProvider | undefined,
126
129
  private readonly registry: IRegistry,
127
130
  private readonly rebalancerConfig: RebalancerConfig,
@@ -146,17 +149,12 @@ export class RebalancerService {
146
149
  this.contextFactory = await RebalancerContextFactory.create(
147
150
  this.rebalancerConfig,
148
151
  this.multiProvider,
152
+ this.inventoryMultiProvider,
149
153
  this.multiProtocolProvider,
150
154
  this.registry,
151
155
  this.logger,
152
156
  );
153
157
 
154
- // Create monitor (always needed for daemon mode)
155
- if (this.mode === 'daemon') {
156
- const checkFrequency = this.config.checkFrequency ?? 60_000;
157
- this.monitor = this.contextFactory.createMonitor(checkFrequency);
158
- }
159
-
160
158
  // Create metrics if enabled
161
159
  if (this.config.withMetrics) {
162
160
  this.metrics = await this.contextFactory.createMetrics(
@@ -168,16 +166,8 @@ export class RebalancerService {
168
166
  // Create strategy
169
167
  this.strategy = await this.contextFactory.createStrategy(this.metrics);
170
168
 
171
- // Create rebalancer (unless in monitor-only mode)
172
- if (!this.config.monitorOnly) {
173
- this.rebalancer = this.contextFactory.createRebalancer(this.metrics);
174
- } else {
175
- this.logger.warn(
176
- 'Running in monitorOnly mode: no transactions will be executed.',
177
- );
178
- }
179
-
180
169
  // Create or use provided ActionTracker for tracking inflight actions
170
+ // Must be created BEFORE rebalancer since rebalancer needs it
181
171
  if (this.config.actionTracker) {
182
172
  // Use externally provided ActionTracker (e.g., for simulation/testing)
183
173
  this.actionTracker = this.config.actionTracker;
@@ -196,14 +186,49 @@ export class RebalancerService {
196
186
  this.logger.info('ActionTracker initialized');
197
187
  }
198
188
 
199
- this.orchestrator = new RebalancerOrchestrator({
200
- strategy: this.strategy!,
201
- rebalancer: this.rebalancer,
189
+ // Create rebalancers (both movableCollateral and inventory if configured)
190
+ let rebalancers: IRebalancer[] = [];
191
+ let externalBridgeRegistry: Partial<ExternalBridgeRegistry> = {};
192
+ let inventoryConfig: InventoryMonitorConfig | undefined;
193
+
194
+ if (!this.config.monitorOnly) {
195
+ const result = await this.contextFactory.createRebalancers(
196
+ this.actionTracker,
197
+ this.metrics,
198
+ );
199
+ rebalancers = result.rebalancers;
200
+ externalBridgeRegistry = result.externalBridgeRegistry;
201
+ inventoryConfig = result.inventoryConfig;
202
+
203
+ if (rebalancers.length > 0) {
204
+ this.logger.info(`${rebalancers.length} rebalancer(s) created`);
205
+ }
206
+ } else {
207
+ this.logger.warn(
208
+ 'Running in monitorOnly mode: no transactions will be executed.',
209
+ );
210
+ }
211
+
212
+ // Set instance variable for backward compatibility with executeManual
213
+ // (Task 5 will remove this when refactoring executeManual)
214
+ if (rebalancers.length > 0) {
215
+ this.rebalancer = rebalancers[0];
216
+ }
217
+
218
+ if (this.mode === 'daemon') {
219
+ const checkFrequency = this.config.checkFrequency ?? 60_000;
220
+ this.monitor = this.contextFactory.createMonitor(
221
+ checkFrequency,
222
+ inventoryConfig,
223
+ );
224
+ }
225
+
226
+ this.orchestrator = this.contextFactory.createOrchestrator({
227
+ strategy: this.strategy,
202
228
  actionTracker: this.actionTracker,
203
229
  inflightContextAdapter: this.inflightContextAdapter,
204
- multiProvider: this.multiProvider,
205
- rebalancerConfig: this.rebalancerConfig,
206
- logger: this.logger,
230
+ rebalancers,
231
+ externalBridgeRegistry: externalBridgeRegistry,
207
232
  metrics: this.metrics,
208
233
  });
209
234
 
@@ -266,14 +291,15 @@ export class RebalancerService {
266
291
  originConfig.override?.[destination]?.bridge ?? originConfig.bridge;
267
292
 
268
293
  try {
269
- const route: RebalanceRoute = {
270
- intentId: randomUUID(),
294
+ const manualRoute: MovableCollateralRoute & { intentId: string } = {
271
295
  origin,
272
296
  destination,
273
297
  amount: BigInt(toWei(amount, originToken.decimals)),
298
+ executionType: 'movableCollateral',
274
299
  bridge,
300
+ intentId: `manual-${Date.now()}`,
275
301
  };
276
- await this.rebalancer.rebalance([route]);
302
+ await this.rebalancer.rebalance([manualRoute]);
277
303
  this.logger.info(
278
304
  `✅ Manual rebalance from ${origin} to ${destination} for amount ${amount} submitted successfully.`,
279
305
  );
@@ -345,7 +371,15 @@ export class RebalancerService {
345
371
  process.exit(0);
346
372
  }
347
373
 
374
+ /**
375
+ * Handle token info events from monitor by delegating to orchestrator
376
+ */
348
377
  private async onTokenInfo(event: MonitorEvent): Promise<void> {
378
+ if (!this.orchestrator) {
379
+ this.logger.error('Orchestrator not initialized');
380
+ return;
381
+ }
382
+
349
383
  await this.orchestrator.executeCycle(event);
350
384
  }
351
385
 
@@ -285,7 +285,6 @@ describe('Collateral Deficit E2E', function () {
285
285
  expect(intentToArbitrum!.destination).to.equal(DOMAIN_IDS.anvil2);
286
286
  expect(intentToArbitrum!.amount).to.equal(400000000n);
287
287
  expect(intentToArbitrum!.status).to.equal('in_progress');
288
- expect(intentToArbitrum!.fulfilledAmount).to.equal(0n);
289
288
 
290
289
  // Capture intent ID for completion verification
291
290
  const rebalanceIntentId = intentToArbitrum!.id;
@@ -357,7 +356,7 @@ describe('Collateral Deficit E2E', function () {
357
356
  hyperlaneCore,
358
357
  {
359
358
  dispatchTx: rebalanceTxReceipt,
360
- messageId: actionToArbitrum!.messageId,
359
+ messageId: actionToArbitrum!.messageId!,
361
360
  origin: 'anvil1',
362
361
  destination: 'anvil2',
363
362
  },
@@ -388,11 +387,10 @@ describe('Collateral Deficit E2E', function () {
388
387
  );
389
388
  expect(completedAction!.status).to.equal('complete');
390
389
 
391
- // Assert: Intent is now complete (fulfilledAmount >= amount)
390
+ // Assert: Intent is now complete
392
391
  const completedIntent =
393
392
  await context.tracker.getRebalanceIntent(rebalanceIntentId);
394
393
  expect(completedIntent!.status).to.equal('complete');
395
- expect(completedIntent!.fulfilledAmount).to.equal(400000000n);
396
394
 
397
395
  // Assert: No more in-progress actions
398
396
  const remainingActions = await context.tracker.getInProgressActions();
@@ -233,7 +233,7 @@ describe('CompositeStrategy E2E', function () {
233
233
  hyperlaneCore,
234
234
  {
235
235
  dispatchTx: rebalanceTxReceipt!,
236
- messageId: action.messageId,
236
+ messageId: action.messageId!,
237
237
  origin: originChain,
238
238
  destination: destChain,
239
239
  },
@@ -432,7 +432,7 @@ describe('CompositeStrategy E2E', function () {
432
432
  hyperlaneCore,
433
433
  {
434
434
  dispatchTx: rebalanceTxReceipt!,
435
- messageId: action.messageId,
435
+ messageId: action.messageId!,
436
436
  origin: originChain,
437
437
  destination: destChain,
438
438
  },
@@ -651,7 +651,7 @@ describe('CompositeStrategy E2E', function () {
651
651
  hyperlaneCore,
652
652
  {
653
653
  dispatchTx: rebalanceTxReceipt!,
654
- messageId: action.messageId,
654
+ messageId: action.messageId!,
655
655
  origin: originChain,
656
656
  destination: destChain,
657
657
  },
@@ -794,7 +794,7 @@ describe('CompositeStrategy E2E', function () {
794
794
  );
795
795
  const relayResult = await tryRelayMessage(multiProvider, hyperlaneCore, {
796
796
  dispatchTx: rebalanceTxReceipt,
797
- messageId: inflightToBase.messageId,
797
+ messageId: inflightToBase.messageId!,
798
798
  origin: 'anvil1',
799
799
  destination: 'anvil3',
800
800
  });
@@ -959,7 +959,7 @@ describe('CompositeStrategy E2E', function () {
959
959
 
960
960
  const relayResult = await tryRelayMessage(multiProvider, hyperlaneCore, {
961
961
  dispatchTx: rebalanceTxReceipt,
962
- messageId: action.messageId,
962
+ messageId: action.messageId!,
963
963
  origin: originChain,
964
964
  destination: destChain,
965
965
  });