@hyperlane-xyz/rebalancer 0.1.2 → 1.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 (223) hide show
  1. package/README.md +134 -14
  2. package/dist/config/RebalancerConfig.d.ts +2 -2
  3. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  4. package/dist/config/RebalancerConfig.js +4 -3
  5. package/dist/config/RebalancerConfig.js.map +1 -1
  6. package/dist/config/RebalancerConfig.test.js +434 -163
  7. package/dist/config/RebalancerConfig.test.js.map +1 -1
  8. package/dist/config/types.d.ts +1650 -290
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +124 -46
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/Rebalancer.d.ts +14 -7
  13. package/dist/core/Rebalancer.d.ts.map +1 -1
  14. package/dist/core/Rebalancer.js +168 -99
  15. package/dist/core/Rebalancer.js.map +1 -1
  16. package/dist/core/Rebalancer.test.d.ts +2 -0
  17. package/dist/core/Rebalancer.test.d.ts.map +1 -0
  18. package/dist/core/Rebalancer.test.js +391 -0
  19. package/dist/core/Rebalancer.test.js.map +1 -0
  20. package/dist/core/RebalancerService.d.ts +16 -2
  21. package/dist/core/RebalancerService.d.ts.map +1 -1
  22. package/dist/core/RebalancerService.js +164 -21
  23. package/dist/core/RebalancerService.js.map +1 -1
  24. package/dist/core/RebalancerService.test.d.ts +2 -0
  25. package/dist/core/RebalancerService.test.d.ts.map +1 -0
  26. package/dist/core/RebalancerService.test.js +809 -0
  27. package/dist/core/RebalancerService.test.js.map +1 -0
  28. package/dist/factories/RebalancerContextFactory.d.ts +11 -0
  29. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  30. package/dist/factories/RebalancerContextFactory.js +60 -13
  31. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  32. package/dist/index.d.ts +6 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/interfaces/IMonitor.d.ts +6 -8
  37. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  38. package/dist/interfaces/IMonitor.js.map +1 -1
  39. package/dist/interfaces/IRebalancer.d.ts +20 -4
  40. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  41. package/dist/interfaces/IStrategy.d.ts +18 -2
  42. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  43. package/dist/metrics/Metrics.d.ts +4 -2
  44. package/dist/metrics/Metrics.d.ts.map +1 -1
  45. package/dist/metrics/Metrics.js +21 -1
  46. package/dist/metrics/Metrics.js.map +1 -1
  47. package/dist/metrics/scripts/metrics.d.ts +2 -0
  48. package/dist/metrics/scripts/metrics.d.ts.map +1 -1
  49. package/dist/metrics/scripts/metrics.js +12 -0
  50. package/dist/metrics/scripts/metrics.js.map +1 -1
  51. package/dist/monitor/Monitor.d.ts +8 -3
  52. package/dist/monitor/Monitor.d.ts.map +1 -1
  53. package/dist/monitor/Monitor.js +75 -15
  54. package/dist/monitor/Monitor.js.map +1 -1
  55. package/dist/strategy/BaseStrategy.d.ts +51 -5
  56. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  57. package/dist/strategy/BaseStrategy.js +199 -19
  58. package/dist/strategy/BaseStrategy.js.map +1 -1
  59. package/dist/strategy/CollateralDeficitStrategy.d.ts +65 -0
  60. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -0
  61. package/dist/strategy/CollateralDeficitStrategy.js +245 -0
  62. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -0
  63. package/dist/strategy/CollateralDeficitStrategy.test.d.ts +2 -0
  64. package/dist/strategy/CollateralDeficitStrategy.test.d.ts.map +1 -0
  65. package/dist/strategy/CollateralDeficitStrategy.test.js +364 -0
  66. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -0
  67. package/dist/strategy/CompositeStrategy.d.ts +18 -0
  68. package/dist/strategy/CompositeStrategy.d.ts.map +1 -0
  69. package/dist/strategy/CompositeStrategy.js +63 -0
  70. package/dist/strategy/CompositeStrategy.js.map +1 -0
  71. package/dist/strategy/CompositeStrategy.test.d.ts +2 -0
  72. package/dist/strategy/CompositeStrategy.test.d.ts.map +1 -0
  73. package/dist/strategy/CompositeStrategy.test.js +265 -0
  74. package/dist/strategy/CompositeStrategy.test.js.map +1 -0
  75. package/dist/strategy/MinAmountStrategy.d.ts +12 -5
  76. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
  77. package/dist/strategy/MinAmountStrategy.js +23 -14
  78. package/dist/strategy/MinAmountStrategy.js.map +1 -1
  79. package/dist/strategy/MinAmountStrategy.test.js +88 -20
  80. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  81. package/dist/strategy/StrategyFactory.d.ts +15 -6
  82. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  83. package/dist/strategy/StrategyFactory.js +48 -10
  84. package/dist/strategy/StrategyFactory.js.map +1 -1
  85. package/dist/strategy/StrategyFactory.test.js +2 -2
  86. package/dist/strategy/StrategyFactory.test.js.map +1 -1
  87. package/dist/strategy/WeightedStrategy.d.ts +13 -4
  88. package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
  89. package/dist/strategy/WeightedStrategy.js +18 -6
  90. package/dist/strategy/WeightedStrategy.js.map +1 -1
  91. package/dist/strategy/WeightedStrategy.test.js +108 -18
  92. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  93. package/dist/strategy/index.d.ts +2 -0
  94. package/dist/strategy/index.d.ts.map +1 -1
  95. package/dist/strategy/index.js +2 -0
  96. package/dist/strategy/index.js.map +1 -1
  97. package/dist/test/helpers.d.ts +93 -3
  98. package/dist/test/helpers.d.ts.map +1 -1
  99. package/dist/test/helpers.js +267 -10
  100. package/dist/test/helpers.js.map +1 -1
  101. package/dist/tracking/ActionTracker.d.ts +49 -0
  102. package/dist/tracking/ActionTracker.d.ts.map +1 -0
  103. package/dist/tracking/ActionTracker.js +422 -0
  104. package/dist/tracking/ActionTracker.js.map +1 -0
  105. package/dist/tracking/ActionTracker.test.d.ts +2 -0
  106. package/dist/tracking/ActionTracker.test.d.ts.map +1 -0
  107. package/dist/tracking/ActionTracker.test.js +637 -0
  108. package/dist/tracking/ActionTracker.test.js.map +1 -0
  109. package/dist/tracking/IActionTracker.d.ts +101 -0
  110. package/dist/tracking/IActionTracker.d.ts.map +1 -0
  111. package/dist/tracking/IActionTracker.js +2 -0
  112. package/dist/tracking/IActionTracker.js.map +1 -0
  113. package/dist/tracking/InflightContextAdapter.d.ts +18 -0
  114. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -0
  115. package/dist/tracking/InflightContextAdapter.js +35 -0
  116. package/dist/tracking/InflightContextAdapter.js.map +1 -0
  117. package/dist/tracking/InflightContextAdapter.test.d.ts +2 -0
  118. package/dist/tracking/InflightContextAdapter.test.d.ts.map +1 -0
  119. package/dist/tracking/InflightContextAdapter.test.js +172 -0
  120. package/dist/tracking/InflightContextAdapter.test.js.map +1 -0
  121. package/dist/tracking/index.d.ts +7 -0
  122. package/dist/tracking/index.d.ts.map +1 -0
  123. package/dist/tracking/index.js +6 -0
  124. package/dist/tracking/index.js.map +1 -0
  125. package/dist/tracking/store/IStore.d.ts +41 -0
  126. package/dist/tracking/store/IStore.d.ts.map +1 -0
  127. package/dist/tracking/store/IStore.js +2 -0
  128. package/dist/tracking/store/IStore.js.map +1 -0
  129. package/dist/tracking/store/InMemoryStore.d.ts +21 -0
  130. package/dist/tracking/store/InMemoryStore.d.ts.map +1 -0
  131. package/dist/tracking/store/InMemoryStore.js +40 -0
  132. package/dist/tracking/store/InMemoryStore.js.map +1 -0
  133. package/dist/tracking/store/InMemoryStore.test.d.ts +2 -0
  134. package/dist/tracking/store/InMemoryStore.test.d.ts.map +1 -0
  135. package/dist/tracking/store/InMemoryStore.test.js +290 -0
  136. package/dist/tracking/store/InMemoryStore.test.js.map +1 -0
  137. package/dist/tracking/store/index.d.ts +3 -0
  138. package/dist/tracking/store/index.d.ts.map +1 -0
  139. package/dist/tracking/store/index.js +2 -0
  140. package/dist/tracking/store/index.js.map +1 -0
  141. package/dist/tracking/types.d.ts +43 -0
  142. package/dist/tracking/types.d.ts.map +1 -0
  143. package/dist/tracking/types.js +2 -0
  144. package/dist/tracking/types.js.map +1 -0
  145. package/dist/utils/ExplorerClient.d.ts +39 -1
  146. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  147. package/dist/utils/ExplorerClient.js +205 -2
  148. package/dist/utils/ExplorerClient.js.map +1 -1
  149. package/dist/utils/balanceUtils.js +2 -2
  150. package/dist/utils/balanceUtils.js.map +1 -1
  151. package/dist/utils/balanceUtils.test.js +1 -0
  152. package/dist/utils/balanceUtils.test.js.map +1 -1
  153. package/dist/utils/bridgeUtils.d.ts +1 -3
  154. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  155. package/dist/utils/bridgeUtils.js +1 -5
  156. package/dist/utils/bridgeUtils.js.map +1 -1
  157. package/dist/utils/bridgeUtils.test.js +3 -14
  158. package/dist/utils/bridgeUtils.test.js.map +1 -1
  159. package/package.json +11 -9
  160. package/src/config/RebalancerConfig.test.ts +459 -163
  161. package/src/config/RebalancerConfig.ts +5 -3
  162. package/src/config/types.ts +159 -52
  163. package/src/core/Rebalancer.test.ts +632 -0
  164. package/src/core/Rebalancer.ts +247 -157
  165. package/src/core/RebalancerService.test.ts +1144 -0
  166. package/src/core/RebalancerService.ts +245 -23
  167. package/src/factories/RebalancerContextFactory.ts +115 -14
  168. package/src/index.ts +16 -4
  169. package/src/interfaces/IMonitor.ts +15 -8
  170. package/src/interfaces/IRebalancer.ts +22 -4
  171. package/src/interfaces/IStrategy.ts +23 -2
  172. package/src/metrics/Metrics.ts +26 -5
  173. package/src/metrics/scripts/metrics.ts +14 -0
  174. package/src/monitor/Monitor.ts +109 -22
  175. package/src/strategy/BaseStrategy.ts +316 -26
  176. package/src/strategy/CollateralDeficitStrategy.test.ts +551 -0
  177. package/src/strategy/CollateralDeficitStrategy.ts +390 -0
  178. package/src/strategy/CompositeStrategy.test.ts +405 -0
  179. package/src/strategy/CompositeStrategy.ts +102 -0
  180. package/src/strategy/MinAmountStrategy.test.ts +189 -88
  181. package/src/strategy/MinAmountStrategy.ts +44 -13
  182. package/src/strategy/StrategyFactory.test.ts +2 -2
  183. package/src/strategy/StrategyFactory.ts +91 -8
  184. package/src/strategy/WeightedStrategy.test.ts +187 -72
  185. package/src/strategy/WeightedStrategy.ts +41 -7
  186. package/src/strategy/index.ts +2 -0
  187. package/src/test/helpers.ts +418 -14
  188. package/src/tracking/ActionTracker.test.ts +783 -0
  189. package/src/tracking/ActionTracker.ts +647 -0
  190. package/src/tracking/IActionTracker.ts +140 -0
  191. package/src/tracking/InflightContextAdapter.test.ts +203 -0
  192. package/src/tracking/InflightContextAdapter.ts +42 -0
  193. package/src/tracking/index.ts +36 -0
  194. package/src/tracking/store/IStore.ts +48 -0
  195. package/src/tracking/store/InMemoryStore.test.ts +338 -0
  196. package/src/tracking/store/InMemoryStore.ts +58 -0
  197. package/src/tracking/store/index.ts +2 -0
  198. package/src/tracking/types.ts +74 -0
  199. package/src/utils/ExplorerClient.ts +266 -3
  200. package/src/utils/balanceUtils.test.ts +1 -0
  201. package/src/utils/balanceUtils.ts +2 -2
  202. package/src/utils/bridgeUtils.test.ts +3 -15
  203. package/src/utils/bridgeUtils.ts +0 -10
  204. package/dist/core/WithInflightGuard.d.ts +0 -20
  205. package/dist/core/WithInflightGuard.d.ts.map +0 -1
  206. package/dist/core/WithInflightGuard.js +0 -47
  207. package/dist/core/WithInflightGuard.js.map +0 -1
  208. package/dist/core/WithInflightGuard.test.d.ts +0 -2
  209. package/dist/core/WithInflightGuard.test.d.ts.map +0 -1
  210. package/dist/core/WithInflightGuard.test.js +0 -64
  211. package/dist/core/WithInflightGuard.test.js.map +0 -1
  212. package/dist/core/WithSemaphore.d.ts +0 -22
  213. package/dist/core/WithSemaphore.d.ts.map +0 -1
  214. package/dist/core/WithSemaphore.js +0 -67
  215. package/dist/core/WithSemaphore.js.map +0 -1
  216. package/dist/core/WithSemaphore.test.d.ts +0 -2
  217. package/dist/core/WithSemaphore.test.d.ts.map +0 -1
  218. package/dist/core/WithSemaphore.test.js +0 -83
  219. package/dist/core/WithSemaphore.test.js.map +0 -1
  220. package/src/core/WithInflightGuard.test.ts +0 -131
  221. package/src/core/WithInflightGuard.ts +0 -67
  222. package/src/core/WithSemaphore.test.ts +0 -111
  223. package/src/core/WithSemaphore.ts +0 -92
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'crypto';
1
2
  import { Logger } from 'pino';
2
3
 
3
4
  import { IRegistry } from '@hyperlane-xyz/registry';
@@ -9,17 +10,34 @@ import {
9
10
  import { assert, toWei } from '@hyperlane-xyz/utils';
10
11
 
11
12
  import { RebalancerConfig } from '../config/RebalancerConfig.js';
13
+ import {
14
+ getStrategyChainConfig,
15
+ getStrategyChainNames,
16
+ } from '../config/types.js';
12
17
  import { RebalancerContextFactory } from '../factories/RebalancerContextFactory.js';
13
18
  import {
19
+ type ConfirmedBlockTags,
14
20
  MonitorEvent,
15
21
  MonitorEventType,
16
22
  MonitorPollingError,
17
23
  MonitorStartError,
18
24
  } from '../interfaces/IMonitor.js';
19
- import type { IRebalancer } from '../interfaces/IRebalancer.js';
20
- import type { IStrategy } from '../interfaces/IStrategy.js';
25
+ import type {
26
+ IRebalancer,
27
+ RebalanceExecutionResult,
28
+ RebalanceRoute,
29
+ } from '../interfaces/IRebalancer.js';
30
+ import type {
31
+ IStrategy,
32
+ InflightContext,
33
+ StrategyRoute,
34
+ } from '../interfaces/IStrategy.js';
21
35
  import { Metrics } from '../metrics/Metrics.js';
22
36
  import { Monitor } from '../monitor/Monitor.js';
37
+ import {
38
+ type IActionTracker,
39
+ InflightContextAdapter,
40
+ } from '../tracking/index.js';
23
41
  import { getRawBalances } from '../utils/balanceUtils.js';
24
42
 
25
43
  export interface RebalancerServiceConfig {
@@ -101,6 +119,8 @@ export class RebalancerService {
101
119
  private rebalancer?: IRebalancer;
102
120
  private metrics?: Metrics;
103
121
  private mode: 'manual' | 'daemon';
122
+ private actionTracker?: IActionTracker;
123
+ private inflightContextAdapter?: InflightContextAdapter;
104
124
 
105
125
  constructor(
106
126
  private readonly multiProvider: MultiProvider,
@@ -159,7 +179,24 @@ export class RebalancerService {
159
179
  );
160
180
  }
161
181
 
162
- this.logger.info('✅ RebalancerService initialized successfully');
182
+ // Create ActionTracker for tracking inflight actions
183
+ const { tracker, adapter } =
184
+ await this.contextFactory.createActionTracker();
185
+ this.actionTracker = tracker;
186
+ this.inflightContextAdapter = adapter;
187
+ await this.actionTracker.initialize();
188
+ this.logger.info('ActionTracker initialized');
189
+
190
+ this.logger.info(
191
+ {
192
+ warpRouteId: this.rebalancerConfig.warpRouteId,
193
+ strategyTypes: this.rebalancerConfig.strategyConfig.map(
194
+ (s) => s.rebalanceStrategy,
195
+ ),
196
+ chains: getStrategyChainNames(this.rebalancerConfig.strategyConfig),
197
+ },
198
+ 'RebalancerService initialized',
199
+ );
163
200
  }
164
201
 
165
202
  /**
@@ -195,14 +232,28 @@ export class RebalancerService {
195
232
  assert(!isNaN(amountNum), 'Amount must be a valid number');
196
233
  assert(amountNum > 0, 'Amount must be greater than 0');
197
234
 
235
+ const originConfig = getStrategyChainConfig(
236
+ this.rebalancerConfig.strategyConfig,
237
+ origin,
238
+ );
239
+ assert(
240
+ originConfig?.bridge,
241
+ `No bridge configured for origin chain ${origin}`,
242
+ );
243
+
244
+ // Use destination-specific bridge override if configured, otherwise use default
245
+ const bridge =
246
+ originConfig.override?.[destination]?.bridge ?? originConfig.bridge;
247
+
198
248
  try {
199
- await this.rebalancer.rebalance([
200
- {
201
- origin,
202
- destination,
203
- amount: BigInt(toWei(amount, originToken.decimals)),
204
- },
205
- ]);
249
+ const route: RebalanceRoute = {
250
+ intentId: randomUUID(),
251
+ origin,
252
+ destination,
253
+ amount: BigInt(toWei(amount, originToken.decimals)),
254
+ bridge,
255
+ };
256
+ await this.rebalancer.rebalance([route]);
206
257
  this.logger.info(
207
258
  `✅ Manual rebalance from ${origin} to ${destination} for amount ${amount} submitted successfully.`,
208
259
  );
@@ -274,10 +325,9 @@ export class RebalancerService {
274
325
  process.exit(0);
275
326
  }
276
327
 
277
- /**
278
- * Event handler for token info updates from monitor
279
- */
280
328
  private async onTokenInfo(event: MonitorEvent): Promise<void> {
329
+ this.logger.info('Polling cycle started');
330
+
281
331
  if (this.metrics) {
282
332
  await Promise.all(
283
333
  event.tokensInfo.map((tokenInfo) =>
@@ -286,24 +336,196 @@ export class RebalancerService {
286
336
  );
287
337
  }
288
338
 
339
+ await this.syncActionTracker(event.confirmedBlockTags);
340
+
289
341
  const rawBalances = getRawBalances(
290
- Object.keys(this.rebalancerConfig.strategyConfig.chains),
342
+ getStrategyChainNames(this.rebalancerConfig.strategyConfig),
291
343
  event,
292
344
  this.logger,
293
345
  );
294
346
 
295
- const rebalancingRoutes = this.strategy!.getRebalancingRoutes(rawBalances);
347
+ this.logger.info(
348
+ {
349
+ balances: Object.entries(rawBalances).map(([chain, balance]) => ({
350
+ chain,
351
+ balance: balance.toString(),
352
+ })),
353
+ },
354
+ 'Router balances',
355
+ );
356
+
357
+ // Get inflight context for strategy decision-making
358
+ const inflightContext = await this.getInflightContext();
296
359
 
297
- this.rebalancer
298
- ?.rebalance(rebalancingRoutes)
299
- .then(() => {
360
+ const strategyRoutes = this.strategy!.getRebalancingRoutes(
361
+ rawBalances,
362
+ inflightContext,
363
+ );
364
+
365
+ if (strategyRoutes.length > 0) {
366
+ this.logger.info(
367
+ {
368
+ routes: strategyRoutes.map((r) => ({
369
+ from: r.origin,
370
+ to: r.destination,
371
+ amount: r.amount.toString(),
372
+ })),
373
+ },
374
+ 'Routes proposed',
375
+ );
376
+ if (this.rebalancer) {
377
+ await this.executeWithTracking(strategyRoutes);
378
+ }
379
+ } else {
380
+ this.logger.info('No rebalancing needed');
381
+ }
382
+
383
+ this.logger.info('Polling cycle completed');
384
+ }
385
+
386
+ private async syncActionTracker(
387
+ confirmedBlockTags?: ConfirmedBlockTags,
388
+ ): Promise<void> {
389
+ if (!this.actionTracker) return;
390
+
391
+ try {
392
+ await Promise.all([
393
+ this.actionTracker.syncTransfers(confirmedBlockTags),
394
+ this.actionTracker.syncRebalanceIntents(),
395
+ this.actionTracker.syncRebalanceActions(confirmedBlockTags),
396
+ ]);
397
+
398
+ await this.actionTracker.logStoreContents();
399
+ } catch (error) {
400
+ this.logger.warn(
401
+ { error },
402
+ 'ActionTracker sync failed, using stale data',
403
+ );
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Get inflight context for strategy decision-making
409
+ */
410
+ private async getInflightContext(): Promise<InflightContext> {
411
+ if (!this.inflightContextAdapter) {
412
+ return { pendingRebalances: [], pendingTransfers: [] };
413
+ }
414
+
415
+ return this.inflightContextAdapter.getInflightContext();
416
+ }
417
+
418
+ /**
419
+ * Execute rebalancing with intent tracking.
420
+ * Creates intents and assigns IDs to routes before execution, then processes results by ID.
421
+ */
422
+ private async executeWithTracking(
423
+ strategyRoutes: StrategyRoute[],
424
+ ): Promise<void> {
425
+ if (!this.rebalancer || !this.actionTracker) {
426
+ this.logger.warn('Rebalancer or ActionTracker not available, skipping');
427
+ return;
428
+ }
429
+
430
+ // 1. Convert strategy routes to rebalance routes with IDs and create intents
431
+ // The route ID is used as the intent ID for direct matching
432
+ const rebalanceRoutes: RebalanceRoute[] = [];
433
+ const intentIds: string[] = [];
434
+
435
+ for (const route of strategyRoutes) {
436
+ const intent = await this.actionTracker.createRebalanceIntent({
437
+ origin: this.multiProvider.getDomainId(route.origin),
438
+ destination: this.multiProvider.getDomainId(route.destination),
439
+ amount: route.amount,
440
+ bridge: route.bridge,
441
+ });
442
+ intentIds.push(intent.id);
443
+ rebalanceRoutes.push({
444
+ ...route,
445
+ intentId: intent.id,
446
+ });
447
+ }
448
+
449
+ this.logger.debug(
450
+ { intentCount: rebalanceRoutes.length },
451
+ 'Created rebalance intents',
452
+ );
453
+
454
+ // 2. Execute rebalance with routes that have IDs
455
+ let results: RebalanceExecutionResult[];
456
+ try {
457
+ results = await this.rebalancer.rebalance(rebalanceRoutes);
458
+ const failedResults = results.filter((r) => !r.success);
459
+ if (failedResults.length > 0) {
460
+ this.metrics?.recordRebalancerFailure();
461
+ this.logger.warn(
462
+ { failureCount: failedResults.length, total: results.length },
463
+ 'Rebalancer cycle completed with failures',
464
+ );
465
+ } else {
300
466
  this.metrics?.recordRebalancerSuccess();
301
467
  this.logger.info('Rebalancer completed a cycle successfully');
302
- })
303
- .catch((error: any) => {
304
- this.metrics?.recordRebalancerFailure();
305
- this.logger.error({ error }, 'Error while rebalancing');
306
- });
468
+ }
469
+ } catch (error: any) {
470
+ this.metrics?.recordRebalancerFailure();
471
+ this.logger.error({ error }, 'Error while rebalancing');
472
+
473
+ // Mark all intents as failed
474
+ await Promise.all(
475
+ intentIds.map((id) => this.actionTracker!.failRebalanceIntent(id)),
476
+ );
477
+ return;
478
+ }
479
+
480
+ // 3. Process results - results have IDs that match intents directly
481
+ await this.processExecutionResults(results);
482
+ }
483
+
484
+ /**
485
+ * Process execution results and update tracking state.
486
+ * Results are matched to intents by the route ID (which equals the intent ID).
487
+ */
488
+ private async processExecutionResults(
489
+ results: RebalanceExecutionResult[],
490
+ ): Promise<void> {
491
+ for (const result of results) {
492
+ const intentId = result.route.intentId;
493
+
494
+ if (result.success && result.messageId) {
495
+ await this.actionTracker!.createRebalanceAction({
496
+ intentId,
497
+ origin: this.multiProvider.getDomainId(result.route.origin),
498
+ destination: this.multiProvider.getDomainId(result.route.destination),
499
+ amount: result.route.amount,
500
+ messageId: result.messageId,
501
+ txHash: result.txHash,
502
+ });
503
+
504
+ this.logger.info(
505
+ {
506
+ intentId,
507
+ messageId: result.messageId,
508
+ txHash: result.txHash,
509
+ origin: result.route.origin,
510
+ destination: result.route.destination,
511
+ },
512
+ 'Rebalance action created successfully',
513
+ );
514
+ } else {
515
+ await this.actionTracker!.failRebalanceIntent(intentId);
516
+
517
+ this.logger.warn(
518
+ {
519
+ intentId,
520
+ success: result.success,
521
+ error: result.error,
522
+ origin: result.route.origin,
523
+ destination: result.route.destination,
524
+ },
525
+ 'Rebalance intent marked as failed',
526
+ );
527
+ }
528
+ }
307
529
  }
308
530
 
309
531
  /**
@@ -3,6 +3,7 @@ import { type Logger } from 'pino';
3
3
  import { IRegistry } from '@hyperlane-xyz/registry';
4
4
  import {
5
5
  type ChainMap,
6
+ HyperlaneCore,
6
7
  MultiProtocolProvider,
7
8
  MultiProvider,
8
9
  type Token,
@@ -11,16 +12,33 @@ import {
11
12
  import { objMap } from '@hyperlane-xyz/utils';
12
13
 
13
14
  import { type RebalancerConfig } from '../config/RebalancerConfig.js';
15
+ import { getAllBridges, getStrategyChainNames } from '../config/types.js';
14
16
  import { Rebalancer } from '../core/Rebalancer.js';
15
- import { WithSemaphore } from '../core/WithSemaphore.js';
16
17
  import type { IRebalancer } from '../interfaces/IRebalancer.js';
17
18
  import type { IStrategy } from '../interfaces/IStrategy.js';
18
19
  import { Metrics } from '../metrics/Metrics.js';
19
20
  import { PriceGetter } from '../metrics/PriceGetter.js';
20
21
  import { Monitor } from '../monitor/Monitor.js';
21
22
  import { StrategyFactory } from '../strategy/StrategyFactory.js';
23
+ import {
24
+ ActionTracker,
25
+ type ActionTrackerConfig,
26
+ type IActionTracker,
27
+ InMemoryStore,
28
+ InflightContextAdapter,
29
+ type RebalanceAction,
30
+ type RebalanceActionStatus,
31
+ type RebalanceIntent,
32
+ type RebalanceIntentStatus,
33
+ type Transfer,
34
+ type TransferStatus,
35
+ } from '../tracking/index.js';
36
+ import { ExplorerClient } from '../utils/ExplorerClient.js';
22
37
  import { isCollateralizedTokenEligibleForRebalancing } from '../utils/index.js';
23
38
 
39
+ const DEFAULT_EXPLORER_URL =
40
+ process.env.EXPLORER_API_URL || 'https://explorer4.hasura.app/v1/graphql';
41
+
24
42
  export class RebalancerContextFactory {
25
43
  /**
26
44
  * @param config - The rebalancer config
@@ -141,10 +159,14 @@ export class RebalancerContextFactory {
141
159
  }
142
160
 
143
161
  public async createStrategy(metrics?: Metrics): Promise<IStrategy> {
162
+ const strategyTypes = this.config.strategyConfig.map(
163
+ (s) => s.rebalanceStrategy,
164
+ );
144
165
  this.logger.debug(
145
166
  {
146
167
  warpRouteId: this.config.warpRouteId,
147
- strategyType: this.config.strategyConfig.rebalanceStrategy,
168
+ strategyTypes,
169
+ strategyCount: this.config.strategyConfig.length,
148
170
  },
149
171
  'Creating Strategy',
150
172
  );
@@ -162,13 +184,8 @@ export class RebalancerContextFactory {
162
184
  { warpRouteId: this.config.warpRouteId },
163
185
  'Creating Rebalancer',
164
186
  );
187
+
165
188
  const rebalancer = new Rebalancer(
166
- objMap(this.config.strategyConfig.chains, (_, v) => ({
167
- bridge: v.bridge,
168
- bridgeMinAcceptedAmount: v.bridgeMinAcceptedAmount ?? 0,
169
- bridgeIsWarp: v.bridgeIsWarp ?? false,
170
- override: v.override,
171
- })),
172
189
  this.warpCore,
173
190
  this.multiProvider.metadata,
174
191
  this.tokensByChainName,
@@ -177,20 +194,104 @@ export class RebalancerContextFactory {
177
194
  metrics,
178
195
  );
179
196
 
180
- // Wrap with semaphore for concurrency control
181
- const withSemaphore = new WithSemaphore(
182
- this.config,
183
- rebalancer,
197
+ return rebalancer;
198
+ }
199
+
200
+ /**
201
+ * Creates an ActionTracker for tracking inflight rebalance actions and user transfers.
202
+ * Returns both the tracker and adapter for use by RebalancerService.
203
+ *
204
+ * @param explorerUrl - Optional explorer URL (defaults to production Hyperlane Explorer)
205
+ */
206
+ public async createActionTracker(
207
+ explorerUrl: string = DEFAULT_EXPLORER_URL,
208
+ ): Promise<{
209
+ tracker: IActionTracker;
210
+ adapter: InflightContextAdapter;
211
+ }> {
212
+ this.logger.debug(
213
+ { warpRouteId: this.config.warpRouteId },
214
+ 'Creating ActionTracker',
215
+ );
216
+
217
+ // 1. Create in-memory stores
218
+ const transferStore = new InMemoryStore<Transfer, TransferStatus>();
219
+ const intentStore = new InMemoryStore<
220
+ RebalanceIntent,
221
+ RebalanceIntentStatus
222
+ >();
223
+ const actionStore = new InMemoryStore<
224
+ RebalanceAction,
225
+ RebalanceActionStatus
226
+ >();
227
+
228
+ // 2. Create ExplorerClient
229
+ const explorerClient = new ExplorerClient(explorerUrl);
230
+
231
+ // 3. Get HyperlaneCore from registry
232
+ const addresses = await this.registry.getAddresses();
233
+ const hyperlaneCore = HyperlaneCore.fromAddressesMap(
234
+ addresses,
235
+ this.multiProvider,
236
+ );
237
+
238
+ // 4. Get rebalancer address from signer
239
+ // Use the first chain in the strategy to get the signer address
240
+ const chainNames = getStrategyChainNames(this.config.strategyConfig);
241
+ if (chainNames.length === 0) {
242
+ throw new Error('No chains configured in strategy');
243
+ }
244
+ const signer = this.multiProvider.getSigner(chainNames[0]);
245
+ const rebalancerAddress = await signer.getAddress();
246
+
247
+ const bridges = getAllBridges(this.config.strategyConfig);
248
+
249
+ // Build router→domain mapping (source of truth for routers and domains)
250
+ const routersByDomain: Record<number, string> = {};
251
+ for (const token of this.warpCore.tokens) {
252
+ const domain = this.multiProvider.getDomainId(token.chainName);
253
+ routersByDomain[domain] = token.addressOrDenom;
254
+ }
255
+
256
+ const trackerConfig: ActionTrackerConfig = {
257
+ routersByDomain,
258
+ bridges,
259
+ rebalancerAddress,
260
+ };
261
+
262
+ // 6. Create ActionTracker
263
+ const tracker = new ActionTracker(
264
+ transferStore,
265
+ intentStore,
266
+ actionStore,
267
+ explorerClient,
268
+ hyperlaneCore,
269
+ trackerConfig,
184
270
  this.logger,
185
271
  );
186
272
 
187
- return withSemaphore;
273
+ // 7. Create InflightContextAdapter
274
+ const adapter = new InflightContextAdapter(tracker, this.multiProvider);
275
+
276
+ this.logger.debug(
277
+ {
278
+ warpRouteId: this.config.warpRouteId,
279
+ routerCount: Object.keys(routersByDomain).length,
280
+ bridgeCount: bridges.length,
281
+ domainCount: Object.keys(routersByDomain).length,
282
+ },
283
+ 'ActionTracker created successfully',
284
+ );
285
+
286
+ return { tracker, adapter };
188
287
  }
189
288
 
190
289
  private async getInitialTotalCollateral(): Promise<bigint> {
191
290
  let initialTotalCollateral = 0n;
192
291
 
193
- const chainNames = new Set(Object.keys(this.config.strategyConfig.chains));
292
+ const chainNames = new Set(
293
+ getStrategyChainNames(this.config.strategyConfig),
294
+ );
194
295
 
195
296
  await Promise.all(
196
297
  this.warpCore.tokens.map(async (token) => {
package/src/index.ts CHANGED
@@ -16,17 +16,18 @@ export type {
16
16
 
17
17
  // Core rebalancing logic
18
18
  export { Rebalancer } from './core/Rebalancer.js';
19
- export { WithInflightGuard } from './core/WithInflightGuard.js';
20
- export { WithSemaphore } from './core/WithSemaphore.js';
21
19
 
22
20
  // Configuration
23
21
  export { RebalancerConfig } from './config/RebalancerConfig.js';
24
22
  export {
23
+ getStrategyChainConfig,
24
+ getStrategyChainNames,
25
25
  RebalancerBaseChainConfigSchema,
26
26
  RebalancerConfigSchema,
27
27
  RebalancerMinAmountConfigSchema,
28
28
  RebalancerMinAmountType,
29
29
  RebalancerStrategyOptions,
30
+ RebalancerStrategySchema,
30
31
  RebalancerWeightedChainConfigSchema,
31
32
  StrategyConfigSchema,
32
33
  } from './config/types.js';
@@ -42,6 +43,7 @@ export type {
42
43
 
43
44
  // Strategy
44
45
  export { BaseStrategy } from './strategy/BaseStrategy.js';
46
+ export { CompositeStrategy } from './strategy/CompositeStrategy.js';
45
47
  export { WeightedStrategy } from './strategy/WeightedStrategy.js';
46
48
  export { MinAmountStrategy } from './strategy/MinAmountStrategy.js';
47
49
  export { StrategyFactory } from './strategy/StrategyFactory.js';
@@ -57,13 +59,20 @@ export { PriceGetter } from './metrics/PriceGetter.js';
57
59
  export type {
58
60
  IRebalancer,
59
61
  PreparedTransaction,
62
+ RebalanceRoute,
63
+ RebalanceExecutionResult,
60
64
  } from './interfaces/IRebalancer.js';
61
65
  export type {
62
66
  IStrategy,
63
- RebalancingRoute,
67
+ StrategyRoute,
64
68
  RawBalances,
69
+ InflightContext,
65
70
  } from './interfaces/IStrategy.js';
66
- export type { IMonitor } from './interfaces/IMonitor.js';
71
+ export type {
72
+ ConfirmedBlockTag,
73
+ ConfirmedBlockTags,
74
+ IMonitor,
75
+ } from './interfaces/IMonitor.js';
67
76
  export {
68
77
  MonitorEventType,
69
78
  MonitorEvent,
@@ -82,5 +91,8 @@ export { getRawBalances } from './utils/balanceUtils.js';
82
91
  export { isCollateralizedTokenEligibleForRebalancing } from './utils/tokenUtils.js';
83
92
  export { ExplorerClient } from './utils/ExplorerClient.js';
84
93
 
94
+ // Tracking
95
+ export { InflightContextAdapter } from './tracking/InflightContextAdapter.js';
96
+
85
97
  // Factory
86
98
  export { RebalancerContextFactory } from './factories/RebalancerContextFactory.js';
@@ -1,4 +1,8 @@
1
- import { type Token } from '@hyperlane-xyz/sdk';
1
+ import {
2
+ type ChainMap,
3
+ EthJsonRpcBlockParameterTag,
4
+ type Token,
5
+ } from '@hyperlane-xyz/sdk';
2
6
 
3
7
  import { WrappedError } from '../utils/errors.js';
4
8
 
@@ -16,17 +20,19 @@ export enum MonitorEventType {
16
20
  Start = 'Start',
17
21
  }
18
22
 
19
- /**
20
- * Represents an event emitted by the monitor containing bridgedSupply and token information.
21
- */
23
+ export type ConfirmedBlockTag =
24
+ | number
25
+ | EthJsonRpcBlockParameterTag
26
+ | undefined;
27
+
28
+ export type ConfirmedBlockTags = ChainMap<ConfirmedBlockTag>;
29
+
22
30
  export type MonitorEvent = {
23
- /**
24
- * Collection of objects containing the information retrieved by the Monitor.
25
- */
26
31
  tokensInfo: {
27
32
  token: Token;
28
33
  bridgedSupply?: bigint;
29
34
  }[];
35
+ confirmedBlockTags: ConfirmedBlockTags;
30
36
  };
31
37
 
32
38
  /**
@@ -35,10 +41,11 @@ export type MonitorEvent = {
35
41
  export interface IMonitor {
36
42
  /**
37
43
  * Allows subscribers to listen to hyperlane's tokens info.
44
+ * Handler can be async - Monitor will await it before starting next cycle.
38
45
  */
39
46
  on(
40
47
  eventName: MonitorEventType.TokenInfo,
41
- fn: (event: MonitorEvent) => void,
48
+ fn: (event: MonitorEvent) => void | Promise<void>,
42
49
  ): this;
43
50
 
44
51
  /**
@@ -3,21 +3,39 @@ import {
3
3
  type TokenAmount,
4
4
  } from '@hyperlane-xyz/sdk';
5
5
 
6
- import { type RebalancingRoute } from './IStrategy.js';
6
+ import type { StrategyRoute } from './IStrategy.js';
7
+
8
+ /**
9
+ * RebalanceRoute extends StrategyRoute with a required intentId for tracking.
10
+ * The intentId is assigned by RebalancerService before execution and links
11
+ * to the corresponding RebalanceIntent in the tracking system.
12
+ */
13
+ export type RebalanceRoute = StrategyRoute & {
14
+ /** Links to the RebalanceIntent that this route fulfills */
15
+ intentId: string;
16
+ };
7
17
 
8
18
  export type PreparedTransaction = {
9
19
  populatedTx: Awaited<
10
20
  ReturnType<EvmMovableCollateralAdapter['populateRebalanceTx']>
11
21
  >;
12
- route: RebalancingRoute;
22
+ route: RebalanceRoute;
13
23
  originTokenAmount: TokenAmount;
14
24
  };
15
25
 
16
26
  export type RebalanceMetrics = {
17
- route: RebalancingRoute;
27
+ route: RebalanceRoute;
18
28
  originTokenAmount: TokenAmount;
19
29
  };
20
30
 
31
+ export type RebalanceExecutionResult = {
32
+ route: RebalanceRoute;
33
+ success: boolean;
34
+ messageId?: string;
35
+ txHash?: string;
36
+ error?: string;
37
+ };
38
+
21
39
  export interface IRebalancer {
22
- rebalance(routes: RebalancingRoute[]): Promise<void>;
40
+ rebalance(routes: RebalanceRoute[]): Promise<RebalanceExecutionResult[]>;
23
41
  }
@@ -1,13 +1,34 @@
1
1
  import { type ChainMap, type ChainName } from '@hyperlane-xyz/sdk';
2
+ import type { Address } from '@hyperlane-xyz/utils';
2
3
 
3
4
  export type RawBalances = ChainMap<bigint>;
4
5
 
5
- export type RebalancingRoute = {
6
+ export interface Route {
6
7
  origin: ChainName;
7
8
  destination: ChainName;
8
9
  amount: bigint;
10
+ }
11
+
12
+ export interface StrategyRoute extends Route {
13
+ bridge: Address;
14
+ }
15
+
16
+ export type InflightContext = {
17
+ /**
18
+ * In-flight rebalances from ActionTracker.
19
+ * Uses Route[] because recovered intents (from Explorer startup recovery)
20
+ * don't have bridge information. Some routes may have bridge at runtime.
21
+ */
22
+ pendingRebalances: Route[];
23
+ pendingTransfers: Route[];
24
+ /** Routes from earlier strategies - always have bridge */
25
+ proposedRebalances?: StrategyRoute[];
9
26
  };
10
27
 
11
28
  export interface IStrategy {
12
- getRebalancingRoutes(rawBalances: RawBalances): RebalancingRoute[];
29
+ readonly name: string;
30
+ getRebalancingRoutes(
31
+ rawBalances: RawBalances,
32
+ inflightContext?: InflightContext,
33
+ ): StrategyRoute[];
13
34
  }