@hyperlane-xyz/rebalancer 2.0.0 → 3.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 (213) 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 +8 -2
  6. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  7. package/dist/config/RebalancerConfig.js +9 -4
  8. package/dist/config/RebalancerConfig.js.map +1 -1
  9. package/dist/config/RebalancerConfig.test.js +135 -1
  10. package/dist/config/RebalancerConfig.test.js.map +1 -1
  11. package/dist/config/types.d.ts +1023 -304
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +113 -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 +892 -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 +1382 -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 +719 -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 +74 -110
  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/ForkIndexer.d.ts.map +1 -1
  51. package/dist/e2e/harness/ForkIndexer.js +1 -0
  52. package/dist/e2e/harness/ForkIndexer.js.map +1 -1
  53. package/dist/e2e/harness/TestHelpers.d.ts.map +1 -1
  54. package/dist/e2e/harness/TestHelpers.js +1 -4
  55. package/dist/e2e/harness/TestHelpers.js.map +1 -1
  56. package/dist/e2e/harness/TestRebalancer.d.ts +1 -1
  57. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  58. package/dist/e2e/harness/TestRebalancer.js +9 -9
  59. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  60. package/dist/e2e/minAmount.e2e-test.js +0 -1
  61. package/dist/e2e/minAmount.e2e-test.js.map +1 -1
  62. package/dist/e2e/weighted.e2e-test.js +0 -1
  63. package/dist/e2e/weighted.e2e-test.js.map +1 -1
  64. package/dist/factories/RebalancerContextFactory.d.ts +48 -6
  65. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  66. package/dist/factories/RebalancerContextFactory.js +171 -17
  67. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  68. package/dist/index.d.ts +6 -6
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/interfaces/IExternalBridge.d.ts +101 -0
  73. package/dist/interfaces/IExternalBridge.d.ts.map +1 -0
  74. package/dist/interfaces/IExternalBridge.js +2 -0
  75. package/dist/interfaces/IExternalBridge.js.map +1 -0
  76. package/dist/interfaces/IMonitor.d.ts +1 -0
  77. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  78. package/dist/interfaces/IRebalancer.d.ts +25 -25
  79. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  80. package/dist/interfaces/IStrategy.d.ts +36 -3
  81. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  82. package/dist/interfaces/IStrategy.js +12 -1
  83. package/dist/interfaces/IStrategy.js.map +1 -1
  84. package/dist/metrics/PriceGetter.js +1 -1
  85. package/dist/metrics/PriceGetter.js.map +1 -1
  86. package/dist/metrics/scripts/metrics.d.ts +3 -3
  87. package/dist/monitor/Monitor.d.ts +12 -2
  88. package/dist/monitor/Monitor.d.ts.map +1 -1
  89. package/dist/monitor/Monitor.js +46 -1
  90. package/dist/monitor/Monitor.js.map +1 -1
  91. package/dist/service.js +40 -17
  92. package/dist/service.js.map +1 -1
  93. package/dist/strategy/BaseStrategy.d.ts +12 -6
  94. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  95. package/dist/strategy/BaseStrategy.js +56 -21
  96. package/dist/strategy/BaseStrategy.js.map +1 -1
  97. package/dist/strategy/CollateralDeficitStrategy.d.ts +1 -1
  98. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
  99. package/dist/strategy/CollateralDeficitStrategy.js +19 -11
  100. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
  101. package/dist/strategy/CollateralDeficitStrategy.test.js +135 -2
  102. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -1
  103. package/dist/strategy/CompositeStrategy.test.js +13 -0
  104. package/dist/strategy/CompositeStrategy.test.js.map +1 -1
  105. package/dist/strategy/MinAmountStrategy.test.js +4 -0
  106. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  107. package/dist/strategy/StrategyFactory.d.ts +2 -1
  108. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  109. package/dist/strategy/StrategyFactory.js +24 -8
  110. package/dist/strategy/StrategyFactory.js.map +1 -1
  111. package/dist/strategy/WeightedStrategy.test.js +6 -0
  112. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  113. package/dist/test/helpers.d.ts +8 -7
  114. package/dist/test/helpers.d.ts.map +1 -1
  115. package/dist/test/helpers.js +23 -5
  116. package/dist/test/helpers.js.map +1 -1
  117. package/dist/test/lifiMocks.d.ts +51 -0
  118. package/dist/test/lifiMocks.d.ts.map +1 -0
  119. package/dist/test/lifiMocks.js +130 -0
  120. package/dist/test/lifiMocks.js.map +1 -0
  121. package/dist/tracking/ActionTracker.d.ts +34 -1
  122. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  123. package/dist/tracking/ActionTracker.js +233 -26
  124. package/dist/tracking/ActionTracker.js.map +1 -1
  125. package/dist/tracking/ActionTracker.test.js +380 -19
  126. package/dist/tracking/ActionTracker.test.js.map +1 -1
  127. package/dist/tracking/IActionTracker.d.ts +48 -3
  128. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  129. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
  130. package/dist/tracking/InflightContextAdapter.js +24 -7
  131. package/dist/tracking/InflightContextAdapter.js.map +1 -1
  132. package/dist/tracking/InflightContextAdapter.test.js +7 -4
  133. package/dist/tracking/InflightContextAdapter.test.js.map +1 -1
  134. package/dist/tracking/types.d.ts +33 -2
  135. package/dist/tracking/types.d.ts.map +1 -1
  136. package/dist/utils/ExplorerClient.d.ts +3 -1
  137. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  138. package/dist/utils/ExplorerClient.js +16 -8
  139. package/dist/utils/ExplorerClient.js.map +1 -1
  140. package/dist/utils/bridgeUtils.d.ts +27 -4
  141. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  142. package/dist/utils/bridgeUtils.js +38 -0
  143. package/dist/utils/bridgeUtils.js.map +1 -1
  144. package/dist/utils/bridgeUtils.test.js +9 -0
  145. package/dist/utils/bridgeUtils.test.js.map +1 -1
  146. package/dist/utils/gasEstimation.d.ts +65 -0
  147. package/dist/utils/gasEstimation.d.ts.map +1 -0
  148. package/dist/utils/gasEstimation.js +176 -0
  149. package/dist/utils/gasEstimation.js.map +1 -0
  150. package/dist/utils/tokenUtils.d.ts +9 -1
  151. package/dist/utils/tokenUtils.d.ts.map +1 -1
  152. package/dist/utils/tokenUtils.js +11 -0
  153. package/dist/utils/tokenUtils.js.map +1 -1
  154. package/package.json +9 -7
  155. package/src/bridges/LiFiBridge.ts +538 -0
  156. package/src/config/RebalancerConfig.test.ts +162 -0
  157. package/src/config/RebalancerConfig.ts +21 -3
  158. package/src/config/types.ts +147 -10
  159. package/src/core/InventoryRebalancer.test.ts +1721 -0
  160. package/src/core/InventoryRebalancer.ts +1265 -0
  161. package/src/core/Rebalancer.test.ts +84 -30
  162. package/src/core/Rebalancer.ts +144 -23
  163. package/src/core/RebalancerOrchestrator.test.ts +869 -0
  164. package/src/core/RebalancerOrchestrator.ts +146 -95
  165. package/src/core/RebalancerService.test.ts +86 -124
  166. package/src/core/RebalancerService.ts +67 -33
  167. package/src/e2e/collateral-deficit.e2e-test.ts +2 -4
  168. package/src/e2e/composite.e2e-test.ts +5 -5
  169. package/src/e2e/harness/BridgeSetup.ts +28 -1
  170. package/src/e2e/harness/ForkIndexer.ts +1 -0
  171. package/src/e2e/harness/TestHelpers.ts +1 -4
  172. package/src/e2e/harness/TestRebalancer.ts +10 -7
  173. package/src/e2e/minAmount.e2e-test.ts +1 -2
  174. package/src/e2e/weighted.e2e-test.ts +1 -2
  175. package/src/factories/RebalancerContextFactory.ts +294 -24
  176. package/src/index.ts +22 -5
  177. package/src/interfaces/IExternalBridge.ts +115 -0
  178. package/src/interfaces/IMonitor.ts +1 -0
  179. package/src/interfaces/IRebalancer.ts +45 -29
  180. package/src/interfaces/IStrategy.ts +50 -3
  181. package/src/metrics/PriceGetter.ts +1 -1
  182. package/src/monitor/Monitor.ts +81 -2
  183. package/src/service.ts +59 -18
  184. package/src/strategy/BaseStrategy.ts +77 -24
  185. package/src/strategy/CollateralDeficitStrategy.test.ts +181 -4
  186. package/src/strategy/CollateralDeficitStrategy.ts +42 -15
  187. package/src/strategy/CompositeStrategy.test.ts +13 -0
  188. package/src/strategy/MinAmountStrategy.test.ts +4 -0
  189. package/src/strategy/StrategyFactory.ts +33 -6
  190. package/src/strategy/WeightedStrategy.test.ts +6 -0
  191. package/src/test/helpers.ts +39 -14
  192. package/src/test/lifiMocks.ts +174 -0
  193. package/src/tracking/ActionTracker.test.ts +443 -19
  194. package/src/tracking/ActionTracker.ts +339 -28
  195. package/src/tracking/IActionTracker.ts +59 -3
  196. package/src/tracking/InflightContextAdapter.test.ts +7 -4
  197. package/src/tracking/InflightContextAdapter.ts +42 -9
  198. package/src/tracking/types.ts +45 -2
  199. package/src/utils/ExplorerClient.ts +27 -10
  200. package/src/utils/bridgeUtils.test.ts +9 -0
  201. package/src/utils/bridgeUtils.ts +75 -6
  202. package/src/utils/gasEstimation.ts +272 -0
  203. package/src/utils/tokenUtils.ts +12 -0
  204. package/dist/tracking/index.d.ts +0 -7
  205. package/dist/tracking/index.d.ts.map +0 -1
  206. package/dist/tracking/index.js +0 -6
  207. package/dist/tracking/index.js.map +0 -1
  208. package/dist/utils/index.d.ts +0 -5
  209. package/dist/utils/index.d.ts.map +0 -1
  210. package/dist/utils/index.js +0 -5
  211. package/dist/utils/index.js.map +0 -1
  212. package/src/tracking/index.ts +0 -36
  213. package/src/utils/index.ts +0 -4
@@ -6,16 +6,20 @@ import Sinon from 'sinon';
6
6
  import type { MultiProvider, Token, WarpCore } from '@hyperlane-xyz/sdk';
7
7
 
8
8
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
9
- import { RebalancerStrategyOptions } from '../config/types.js';
9
+ import {
10
+ DEFAULT_INTENT_TTL_MS,
11
+ RebalancerStrategyOptions,
12
+ } from '../config/types.js';
10
13
  import { RebalancerContextFactory } from '../factories/RebalancerContextFactory.js';
14
+ import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
11
15
  import { MonitorEventType } from '../interfaces/IMonitor.js';
12
16
  import type { IRebalancer } from '../interfaces/IRebalancer.js';
13
17
  import type { IStrategy } from '../interfaces/IStrategy.js';
14
18
  import { Metrics } from '../metrics/Metrics.js';
15
19
  import { Monitor } from '../monitor/Monitor.js';
16
20
  import { TEST_ADDRESSES, getTestAddress } from '../test/helpers.js';
17
- import type { IActionTracker } from '../tracking/index.js';
18
- import { InflightContextAdapter } from '../tracking/index.js';
21
+ import type { IActionTracker } from '../tracking/IActionTracker.js';
22
+ import { InflightContextAdapter } from '../tracking/InflightContextAdapter.js';
19
23
 
20
24
  import {
21
25
  RebalancerService,
@@ -46,6 +50,7 @@ function createMockRebalancerConfig(): RebalancerConfig {
46
50
  },
47
51
  },
48
52
  ],
53
+ intentTTL: DEFAULT_INTENT_TTL_MS,
49
54
  } as RebalancerConfig;
50
55
  }
51
56
 
@@ -85,6 +90,7 @@ function createMockWarpCore(): WarpCore {
85
90
 
86
91
  function createMockRebalancer(): IRebalancer & { rebalance: Sinon.SinonStub } {
87
92
  return {
93
+ rebalancerType: 'movableCollateral' as const,
88
94
  rebalance: Sinon.stub().resolves([]),
89
95
  };
90
96
  }
@@ -114,6 +120,10 @@ function createMockActionTracker(): IActionTracker {
114
120
  syncTransfers: Sinon.stub().resolves(),
115
121
  syncRebalanceIntents: Sinon.stub().resolves(),
116
122
  syncRebalanceActions: Sinon.stub().resolves(),
123
+ syncInventoryMovementActions: Sinon.stub().resolves({
124
+ completed: 0,
125
+ failed: 0,
126
+ }),
117
127
  logStoreContents: Sinon.stub().resolves(),
118
128
  getInProgressTransfers: Sinon.stub().resolves([]),
119
129
  getActiveRebalanceIntents: Sinon.stub().resolves([]),
@@ -123,6 +133,10 @@ function createMockActionTracker(): IActionTracker {
123
133
  getRebalanceIntent: Sinon.stub().resolves(undefined),
124
134
  getRebalanceAction: Sinon.stub().resolves(undefined),
125
135
  getInProgressActions: Sinon.stub().resolves([]),
136
+ getPartiallyFulfilledInventoryIntents: Sinon.stub().resolves([]),
137
+ getActionsByType: Sinon.stub().resolves([]),
138
+ getActionsForIntent: Sinon.stub().resolves([]),
139
+ getInflightInventoryMovements: Sinon.stub().resolves(0n),
126
140
  };
127
141
  }
128
142
 
@@ -168,7 +182,12 @@ function createMockContextFactory(
168
182
  getWarpCore: () => warpCore,
169
183
  getTokenForChain: (chain: string) =>
170
184
  warpCore.tokens.find((t) => t.chainName === chain),
171
- createRebalancer: () => rebalancer,
185
+ createRebalancer: (_actionTracker: IActionTracker) => rebalancer,
186
+ createRebalancers: async (_actionTracker: IActionTracker) => ({
187
+ rebalancers: [rebalancer],
188
+ externalBridgeRegistry: {},
189
+ inventoryConfig: undefined,
190
+ }),
172
191
  createStrategy: async () => strategy,
173
192
  createMonitor: () => monitor,
174
193
  createMetrics: async () => overrides.metrics ?? ({} as Metrics),
@@ -176,6 +195,33 @@ function createMockContextFactory(
176
195
  tracker: actionTracker,
177
196
  adapter: inflightAdapter,
178
197
  }),
198
+ createOrchestrator: (options: {
199
+ strategy: IStrategy;
200
+ actionTracker: IActionTracker;
201
+ inflightContextAdapter: InflightContextAdapter;
202
+ rebalancers: IRebalancer[];
203
+ externalBridgeRegistry: Partial<ExternalBridgeRegistry>;
204
+ metrics?: Metrics;
205
+ }) => ({
206
+ executeCycle: Sinon.stub().callsFake(async (_event: any) => {
207
+ // Simulate orchestrator behavior: call strategy, then rebalancer, then record metrics
208
+ const strategyWithGetRoutes = options.strategy as any;
209
+ const routes = strategyWithGetRoutes.getRebalancingRoutes?.() ?? [];
210
+ if (routes.length > 0 && options.rebalancers[0]) {
211
+ const results = await options.rebalancers[0].rebalance(routes);
212
+ if (options.metrics && results) {
213
+ results.forEach((result: any) => {
214
+ if (result.success) {
215
+ (options.metrics as any).recordRebalancerSuccess?.();
216
+ } else {
217
+ (options.metrics as any).recordRebalancerFailure?.();
218
+ }
219
+ });
220
+ }
221
+ }
222
+ return { success: true };
223
+ }),
224
+ }),
179
225
  } as unknown as RebalancerContextFactory;
180
226
  }
181
227
 
@@ -189,13 +235,11 @@ interface DaemonTestSetup {
189
235
  async function setupDaemonTest(
190
236
  sandbox: Sinon.SinonSandbox,
191
237
  options: {
192
- intentIds?: string[];
193
238
  rebalanceResults: Array<{
194
239
  route: {
195
240
  origin: string;
196
241
  destination: string;
197
242
  amount: bigint;
198
- intentId: string;
199
243
  bridge: string;
200
244
  };
201
245
  success: boolean;
@@ -212,14 +256,6 @@ async function setupDaemonTest(
212
256
  },
213
257
  ): Promise<DaemonTestSetup> {
214
258
  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
259
 
224
260
  const rebalancer = createMockRebalancer();
225
261
  rebalancer.rebalance.resolves(options.rebalanceResults);
@@ -253,6 +289,7 @@ async function setupDaemonTest(
253
289
  const service = new RebalancerService(
254
290
  createMockMultiProvider(),
255
291
  undefined,
292
+ undefined,
256
293
  {} as any,
257
294
  createMockRebalancerConfig(),
258
295
  { mode: 'daemon', checkFrequency: 60000, logger: testLogger },
@@ -312,6 +349,7 @@ describe('RebalancerService', () => {
312
349
  const service = new RebalancerService(
313
350
  createMockMultiProvider(),
314
351
  undefined,
352
+ undefined,
315
353
  {} as any,
316
354
  createMockRebalancerConfig(),
317
355
  config,
@@ -347,6 +385,7 @@ describe('RebalancerService', () => {
347
385
  const service = new RebalancerService(
348
386
  createMockMultiProvider(),
349
387
  undefined,
388
+ undefined,
350
389
  {} as any,
351
390
  createMockRebalancerConfig(),
352
391
  config,
@@ -373,6 +412,7 @@ describe('RebalancerService', () => {
373
412
  const service = new RebalancerService(
374
413
  createMockMultiProvider(),
375
414
  undefined,
415
+ undefined,
376
416
  {} as any,
377
417
  createMockRebalancerConfig(),
378
418
  config,
@@ -399,6 +439,7 @@ describe('RebalancerService', () => {
399
439
  const service = new RebalancerService(
400
440
  createMockMultiProvider(),
401
441
  undefined,
442
+ undefined,
402
443
  {} as any,
403
444
  createMockRebalancerConfig(),
404
445
  config,
@@ -439,6 +480,7 @@ describe('RebalancerService', () => {
439
480
  },
440
481
  },
441
482
  ],
483
+ intentTTL: DEFAULT_INTENT_TTL_MS,
442
484
  } as RebalancerConfig;
443
485
 
444
486
  const config: RebalancerServiceConfig = {
@@ -449,6 +491,7 @@ describe('RebalancerService', () => {
449
491
  const service = new RebalancerService(
450
492
  createMockMultiProvider(),
451
493
  undefined,
494
+ undefined,
452
495
  {} as any,
453
496
  configWithoutBridge,
454
497
  config,
@@ -476,6 +519,7 @@ describe('RebalancerService', () => {
476
519
  const service = new RebalancerService(
477
520
  createMockMultiProvider(),
478
521
  undefined,
522
+ undefined,
479
523
  {} as any,
480
524
  createMockRebalancerConfig(),
481
525
  config,
@@ -505,6 +549,7 @@ describe('RebalancerService', () => {
505
549
  const service = new RebalancerService(
506
550
  createMockMultiProvider(),
507
551
  undefined,
552
+ undefined,
508
553
  {} as any,
509
554
  createMockRebalancerConfig(),
510
555
  config,
@@ -530,6 +575,7 @@ describe('RebalancerService', () => {
530
575
  const service = new RebalancerService(
531
576
  createMockMultiProvider(),
532
577
  undefined,
578
+ undefined,
533
579
  {} as any,
534
580
  createMockRebalancerConfig(),
535
581
  config,
@@ -559,6 +605,7 @@ describe('RebalancerService', () => {
559
605
  const service = new RebalancerService(
560
606
  createMockMultiProvider(),
561
607
  undefined,
608
+ undefined,
562
609
  {} as any,
563
610
  createMockRebalancerConfig(),
564
611
  config,
@@ -591,6 +638,7 @@ describe('RebalancerService', () => {
591
638
  const service = new RebalancerService(
592
639
  createMockMultiProvider(),
593
640
  undefined,
641
+ undefined,
594
642
  {} as any,
595
643
  createMockRebalancerConfig(),
596
644
  config,
@@ -674,6 +722,7 @@ describe('RebalancerService', () => {
674
722
  const service = new RebalancerService(
675
723
  createMockMultiProvider(),
676
724
  undefined,
725
+ undefined,
677
726
  {} as any,
678
727
  createMockRebalancerConfig(),
679
728
  config,
@@ -766,6 +815,7 @@ describe('RebalancerService', () => {
766
815
  const service = new RebalancerService(
767
816
  createMockMultiProvider(),
768
817
  undefined,
818
+ undefined,
769
819
  {} as any,
770
820
  createMockRebalancerConfig(),
771
821
  config,
@@ -875,6 +925,7 @@ describe('RebalancerService', () => {
875
925
  const service = new RebalancerService(
876
926
  createMockMultiProvider(),
877
927
  undefined,
928
+ undefined,
878
929
  {} as any,
879
930
  createMockRebalancerConfig(),
880
931
  config,
@@ -891,65 +942,24 @@ describe('RebalancerService', () => {
891
942
  });
892
943
 
893
944
  expect(recordRebalancerFailure.calledOnce).to.be.true;
894
- expect(recordRebalancerSuccess.called).to.be.false;
945
+ expect(recordRebalancerSuccess.calledOnce).to.be.true;
895
946
  });
896
947
  });
897
948
 
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'],
949
+ describe('daemon mode rebalancer calls', () => {
950
+ it('should call rebalancer with routes from strategy', async () => {
951
+ const { rebalancer, triggerCycle } = await setupDaemonTest(sandbox, {
939
952
  rebalanceResults: [
940
953
  {
941
954
  route: {
942
955
  origin: 'ethereum',
943
956
  destination: 'arbitrum',
944
957
  amount: 1000n,
945
- intentId: 'intent-456',
946
958
  bridge: TEST_ADDRESSES.bridge,
947
959
  },
948
960
  success: true,
949
961
  messageId:
950
962
  '0x1111111111111111111111111111111111111111111111111111111111111111',
951
- txHash:
952
- '0x2222222222222222222222222222222222222222222222222222222222222222',
953
963
  },
954
964
  ],
955
965
  strategyRoutes: [
@@ -964,42 +974,33 @@ describe('RebalancerService', () => {
964
974
 
965
975
  await triggerCycle();
966
976
 
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');
977
+ expect(rebalancer.rebalance.calledOnce).to.be.true;
978
+ const routesPassedToRebalancer = rebalancer.rebalance.firstCall.args[0];
979
+ expect(routesPassedToRebalancer).to.have.lengthOf(1);
980
+ expect(routesPassedToRebalancer[0].origin).to.equal('ethereum');
981
+ expect(routesPassedToRebalancer[0].destination).to.equal('arbitrum');
973
982
  });
974
983
 
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'],
984
+ it('should call rebalancer with multiple routes', async () => {
985
+ const { rebalancer, triggerCycle } = await setupDaemonTest(sandbox, {
978
986
  rebalanceResults: [
979
987
  {
980
988
  route: {
981
989
  origin: 'ethereum',
982
990
  destination: 'arbitrum',
983
991
  amount: 1000n,
984
- intentId: 'intent-1',
985
992
  bridge: TEST_ADDRESSES.bridge,
986
993
  },
987
994
  success: true,
988
- messageId:
989
- '0x1111111111111111111111111111111111111111111111111111111111111111',
990
- txHash:
991
- '0x2222222222222222222222222222222222222222222222222222222222222222',
992
995
  },
993
996
  {
994
997
  route: {
995
998
  origin: 'arbitrum',
996
999
  destination: 'ethereum',
997
1000
  amount: 500n,
998
- intentId: 'intent-2',
999
1001
  bridge: TEST_ADDRESSES.bridge,
1000
1002
  },
1001
- success: false,
1002
- error: 'Insufficient funds',
1003
+ success: true,
1003
1004
  },
1004
1005
  ],
1005
1006
  strategyRoutes: [
@@ -1020,61 +1021,20 @@ describe('RebalancerService', () => {
1020
1021
 
1021
1022
  await triggerCycle();
1022
1023
 
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;
1024
+ expect(rebalancer.rebalance.calledOnce).to.be.true;
1025
+ const routesPassedToRebalancer = rebalancer.rebalance.firstCall.args[0];
1026
+ expect(routesPassedToRebalancer).to.have.lengthOf(2);
1040
1027
  });
1041
1028
 
1042
- it('should assign intentId from createRebalanceIntent to route before calling rebalancer', async () => {
1029
+ it('should not call rebalancer when no routes proposed', async () => {
1043
1030
  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
- ],
1031
+ rebalanceResults: [],
1032
+ strategyRoutes: [],
1067
1033
  });
1068
1034
 
1069
1035
  await triggerCycle();
1070
1036
 
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
- );
1037
+ expect(rebalancer.rebalance.called).to.be.false;
1078
1038
  });
1079
1039
  });
1080
1040
 
@@ -1093,6 +1053,7 @@ describe('RebalancerService', () => {
1093
1053
  const service = new RebalancerService(
1094
1054
  createMockMultiProvider(),
1095
1055
  undefined,
1056
+ undefined,
1096
1057
  {} as any,
1097
1058
  createMockRebalancerConfig(),
1098
1059
  config,
@@ -1130,6 +1091,7 @@ describe('RebalancerService', () => {
1130
1091
  const service = new RebalancerService(
1131
1092
  createMockMultiProvider(),
1132
1093
  undefined,
1094
+ undefined,
1133
1095
  {} as any,
1134
1096
  createMockRebalancerConfig(),
1135
1097
  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();