@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
@@ -5,6 +5,7 @@ import type { MultiProtocolCore } from '@hyperlane-xyz/sdk';
5
5
  import type { Address, Domain } from '@hyperlane-xyz/utils';
6
6
  import { parseWarpRouteMessage } from '@hyperlane-xyz/utils';
7
7
 
8
+ import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
8
9
  import type { ConfirmedBlockTags } from '../interfaces/IMonitor.js';
9
10
  import type {
10
11
  ExplorerMessage,
@@ -18,9 +19,11 @@ import type {
18
19
  IActionTracker,
19
20
  } from './IActionTracker.js';
20
21
  import type {
22
+ ActionType,
21
23
  IRebalanceActionStore,
22
24
  IRebalanceIntentStore,
23
25
  ITransferStore,
26
+ PartialInventoryIntent,
24
27
  RebalanceAction,
25
28
  RebalanceIntent,
26
29
  Transfer,
@@ -30,6 +33,7 @@ export interface ActionTrackerConfig {
30
33
  routersByDomain: Record<number, string>; // Domain ID → router address (source of truth for routers and domains)
31
34
  bridges: Address[]; // Bridge contract addresses for rebalance action queries
32
35
  rebalancerAddress: Address;
36
+ inventorySignerAddress?: Address; // Optional - for excluding inventory signer from user transfers query
33
37
  }
34
38
 
35
39
  /**
@@ -68,6 +72,7 @@ export class ActionTracker implements IActionTracker {
68
72
  bridges: this.config.bridges,
69
73
  routersByDomain: this.config.routersByDomain,
70
74
  rebalancerAddress: this.config.rebalancerAddress,
75
+ inventorySignerAddress: this.config.inventorySignerAddress,
71
76
  },
72
77
  this.logger,
73
78
  );
@@ -98,10 +103,16 @@ export class ActionTracker implements IActionTracker {
98
103
  async syncTransfers(confirmedBlockTags?: ConfirmedBlockTags): Promise<void> {
99
104
  this.logger.debug('Syncing transfers');
100
105
 
106
+ // Build list of addresses to exclude (rebalancer + optional inventory signer)
107
+ const excludeTxSenders = [this.config.rebalancerAddress];
108
+ if (this.config.inventorySignerAddress) {
109
+ excludeTxSenders.push(this.config.inventorySignerAddress);
110
+ }
111
+
101
112
  const inflightMessages = await this.explorerClient.getInflightUserTransfers(
102
113
  {
103
114
  routersByDomain: this.config.routersByDomain,
104
- excludeTxSender: this.config.rebalancerAddress,
115
+ excludeTxSenders,
105
116
  },
106
117
  this.logger,
107
118
  );
@@ -200,11 +211,12 @@ export class ActionTracker implements IActionTracker {
200
211
  async syncRebalanceIntents(): Promise<void> {
201
212
  this.logger.debug('Syncing rebalance intents');
202
213
 
203
- // Check in_progress intents for completion
214
+ // Check in_progress intents for completion by deriving from action states
204
215
  const inProgressIntents =
205
216
  await this.rebalanceIntentStore.getByStatus('in_progress');
206
217
  for (const intent of inProgressIntents) {
207
- if (intent.fulfilledAmount >= intent.amount) {
218
+ const completedAmount = await this.getCompletedAmountForIntent(intent.id);
219
+ if (completedAmount >= intent.amount) {
208
220
  await this.rebalanceIntentStore.update(intent.id, {
209
221
  status: 'complete',
210
222
  });
@@ -229,6 +241,7 @@ export class ActionTracker implements IActionTracker {
229
241
  bridges: this.config.bridges,
230
242
  routersByDomain: this.config.routersByDomain,
231
243
  rebalancerAddress: this.config.rebalancerAddress,
244
+ inventorySignerAddress: this.config.inventorySignerAddress,
232
245
  },
233
246
  this.logger,
234
247
  );
@@ -257,9 +270,17 @@ export class ActionTracker implements IActionTracker {
257
270
  }
258
271
  }
259
272
 
273
+ // Check delivery status for all in-progress actions in our store
274
+ // Only check delivery for actions that have a messageId (rebalance_message, inventory_deposit)
275
+ // inventory_movement actions are synced separately via LiFi status API
260
276
  const inProgressActions =
261
277
  await this.rebalanceActionStore.getByStatus('in_progress');
262
278
  for (const action of inProgressActions) {
279
+ // Skip actions without messageId (e.g., inventory_movement)
280
+ if (!action.messageId) {
281
+ continue;
282
+ }
283
+
263
284
  const blockTag = await this.getConfirmedBlockTag(
264
285
  action.destination,
265
286
  confirmedBlockTags,
@@ -333,17 +354,23 @@ export class ActionTracker implements IActionTracker {
333
354
  origin: params.origin,
334
355
  destination: params.destination,
335
356
  amount: params.amount,
336
- fulfilledAmount: 0n,
337
357
  bridge: params.bridge,
338
358
  priority: params.priority,
339
359
  strategyType: params.strategyType,
360
+ executionMethod: params.executionMethod,
361
+ externalBridge: params.externalBridge,
340
362
  createdAt: Date.now(),
341
363
  updatedAt: Date.now(),
342
364
  };
343
365
 
344
366
  await this.rebalanceIntentStore.save(intent);
345
367
  this.logger.debug(
346
- { id: intent.id, origin: intent.origin, destination: intent.destination },
368
+ {
369
+ id: intent.id,
370
+ origin: intent.origin,
371
+ destination: intent.destination,
372
+ executionMethod: intent.executionMethod,
373
+ },
347
374
  'Created RebalanceIntent',
348
375
  );
349
376
 
@@ -383,9 +410,12 @@ export class ActionTracker implements IActionTracker {
383
410
  const action: RebalanceAction = {
384
411
  id: uuidv4(),
385
412
  status: 'in_progress',
413
+ type: params.type,
386
414
  intentId: params.intentId,
387
415
  messageId: params.messageId,
388
416
  txHash: params.txHash,
417
+ externalBridgeTransferId: params.externalBridgeTransferId,
418
+ externalBridgeId: params.externalBridgeId,
389
419
  origin: params.origin,
390
420
  destination: params.destination,
391
421
  amount: params.amount,
@@ -408,7 +438,7 @@ export class ActionTracker implements IActionTracker {
408
438
  }
409
439
 
410
440
  this.logger.debug(
411
- { id: action.id, intentId: action.intentId },
441
+ { id: action.id, intentId: action.intentId, type: action.type },
412
442
  'Created RebalanceAction',
413
443
  );
414
444
 
@@ -423,32 +453,262 @@ export class ActionTracker implements IActionTracker {
423
453
 
424
454
  await this.rebalanceActionStore.update(id, { status: 'complete' });
425
455
 
426
- // Update parent intent's fulfilledAmount
427
- const intent = await this.rebalanceIntentStore.get(action.intentId);
428
- if (intent) {
429
- const newFulfilledAmount = intent.fulfilledAmount + action.amount;
430
- const updates: Partial<RebalanceIntent> = {
431
- fulfilledAmount: newFulfilledAmount,
432
- };
456
+ // Check if parent intent is now complete (derive from action states)
457
+ await this.checkAndCompleteIntent(action.intentId);
458
+
459
+ this.logger.info(
460
+ { id, intentId: action.intentId, type: action.type },
461
+ 'Action completed',
462
+ );
463
+ }
464
+
465
+ /**
466
+ * Check if an intent is fully fulfilled based on completed action amounts.
467
+ * Only `inventory_deposit` and `rebalance_message` actions count toward fulfillment.
468
+ */
469
+ private async checkAndCompleteIntent(intentId: string): Promise<void> {
470
+ const intent = await this.rebalanceIntentStore.get(intentId);
471
+ if (!intent || intent.status === 'complete') return;
472
+
473
+ const completedAmount = await this.getCompletedAmountForIntent(intentId);
474
+
475
+ if (completedAmount >= intent.amount) {
476
+ await this.rebalanceIntentStore.update(intentId, { status: 'complete' });
477
+ this.logger.debug(
478
+ { intentId, completedAmount: completedAmount.toString() },
479
+ 'RebalanceIntent fully fulfilled',
480
+ );
481
+ }
482
+ }
433
483
 
434
- // Check if intent is now complete
435
- if (newFulfilledAmount >= intent.amount) {
436
- updates.status = 'complete';
484
+ /**
485
+ * Get the total completed amount for an intent from its actions.
486
+ * Only `inventory_deposit` and `rebalance_message` actions count.
487
+ */
488
+ private async getCompletedAmountForIntent(intentId: string): Promise<bigint> {
489
+ const actions = await this.getActionsForIntent(intentId);
490
+ return actions
491
+ .filter(
492
+ (a) =>
493
+ a.status === 'complete' &&
494
+ (a.type === 'inventory_deposit' || a.type === 'rebalance_message'),
495
+ )
496
+ .reduce((sum, a) => sum + a.amount, 0n);
497
+ }
498
+
499
+ async failRebalanceAction(id: string): Promise<void> {
500
+ await this.rebalanceActionStore.update(id, { status: 'failed' });
501
+ this.logger.info({ id }, 'Action failed');
502
+ }
503
+
504
+ // === RebalanceAction Queries ===
505
+
506
+ async getActionsByType(type: ActionType): Promise<RebalanceAction[]> {
507
+ const allActions = await this.rebalanceActionStore.getAll();
508
+ return allActions.filter((action) => action.type === type);
509
+ }
510
+
511
+ async getInflightInventoryMovements(origin: Domain): Promise<bigint> {
512
+ const allActions = await this.rebalanceActionStore.getAll();
513
+ const inflightMovements = allActions.filter(
514
+ (action) =>
515
+ action.type === 'inventory_movement' &&
516
+ action.status === 'in_progress' &&
517
+ action.origin === origin,
518
+ );
519
+
520
+ return inflightMovements.reduce(
521
+ (sum, action) => sum + action.amount,
522
+ BigInt(0),
523
+ );
524
+ }
525
+
526
+ /**
527
+ * Get inventory intents that are in_progress or not_started but not fully fulfilled,
528
+ * and have no in-flight actions (safe to continue).
529
+ * Returns enriched data with computed values derived from action states.
530
+ *
531
+ * NOTE: We include 'not_started' intents because they may have been created
532
+ * but failed to execute (e.g., all bridges failed viability check). Without
533
+ * checking for these, we would create duplicate intents every polling cycle.
534
+ */
535
+ async getPartiallyFulfilledInventoryIntents(): Promise<
536
+ PartialInventoryIntent[]
537
+ > {
538
+ // Query both in_progress AND not_started intents
539
+ // not_started intents may exist if execution failed before any action was created
540
+ const [inProgressIntents, notStartedIntents] = await Promise.all([
541
+ this.rebalanceIntentStore.getByStatus('in_progress'),
542
+ this.rebalanceIntentStore.getByStatus('not_started'),
543
+ ]);
544
+
545
+ const allActiveIntents = [...inProgressIntents, ...notStartedIntents];
546
+ const partialIntents: PartialInventoryIntent[] = [];
547
+
548
+ for (const intent of allActiveIntents) {
549
+ // Only inventory execution method
550
+ if (intent.executionMethod !== 'inventory') continue;
551
+
552
+ const actions = await this.getActionsForIntent(intent.id);
553
+
554
+ // Check for in-flight inventory_movement actions
555
+ // Skip intents that have a bridge in progress - wait for it to complete
556
+ const hasInflightMovement = actions.some(
557
+ (a) => a.status === 'in_progress' && a.type === 'inventory_movement',
558
+ );
559
+
560
+ if (hasInflightMovement) {
437
561
  this.logger.debug(
438
562
  { intentId: intent.id },
439
- 'RebalanceIntent fully fulfilled',
563
+ 'Skipping partial intent - has in-flight inventory movement',
440
564
  );
565
+ continue;
441
566
  }
442
567
 
443
- await this.rebalanceIntentStore.update(intent.id, updates);
568
+ // Compute amounts from action states
569
+ const completedAmount = actions
570
+ .filter(
571
+ (a) => a.status === 'complete' && a.type === 'inventory_deposit',
572
+ )
573
+ .reduce((sum, a) => sum + a.amount, 0n);
574
+
575
+ const inflightAmount = actions
576
+ .filter(
577
+ (a) => a.status === 'in_progress' && a.type === 'inventory_deposit',
578
+ )
579
+ .reduce((sum, a) => sum + a.amount, 0n);
580
+
581
+ const remaining = intent.amount - completedAmount - inflightAmount;
582
+
583
+ // Safe to continue if: remaining > 0 AND no in-flight inventory_deposit
584
+ if (remaining > 0n && inflightAmount === 0n) {
585
+ partialIntents.push({ intent, completedAmount, remaining });
586
+ }
444
587
  }
445
588
 
446
- this.logger.info({ id, intentId: action.intentId }, 'Action completed');
589
+ return partialIntents;
447
590
  }
448
591
 
449
- async failRebalanceAction(id: string): Promise<void> {
450
- await this.rebalanceActionStore.update(id, { status: 'failed' });
451
- this.logger.info({ id }, 'Action failed');
592
+ /**
593
+ * Get all actions associated with a specific intent.
594
+ */
595
+ async getActionsForIntent(intentId: string): Promise<RebalanceAction[]> {
596
+ const allActions = await this.rebalanceActionStore.getAll();
597
+ return allActions.filter((a) => a.intentId === intentId);
598
+ }
599
+
600
+ async syncInventoryMovementActions(
601
+ externalBridgeRegistry: Partial<ExternalBridgeRegistry>,
602
+ ): Promise<{ completed: number; failed: number }> {
603
+ this.logger.debug('Syncing inventory movement actions');
604
+
605
+ let completed = 0;
606
+ let failed = 0;
607
+
608
+ // Get all in-progress inventory_movement actions
609
+ const inProgressActions =
610
+ await this.rebalanceActionStore.getByStatus('in_progress');
611
+ const inventoryMovements = inProgressActions.filter(
612
+ (a) => a.type === 'inventory_movement',
613
+ );
614
+
615
+ this.logger.debug(
616
+ { count: inventoryMovements.length },
617
+ 'Found in-progress inventory movements',
618
+ );
619
+
620
+ for (const action of inventoryMovements) {
621
+ // Skip if no txHash (shouldn't happen but be safe)
622
+ if (!action.txHash) {
623
+ this.logger.warn(
624
+ { actionId: action.id },
625
+ 'Inventory movement action has no txHash',
626
+ );
627
+ continue;
628
+ }
629
+
630
+ // Skip if no externalBridgeId (shouldn't happen but be safe)
631
+ if (!action.externalBridgeId) {
632
+ this.logger.warn(
633
+ { actionId: action.id },
634
+ 'Inventory movement action has no externalBridgeId',
635
+ );
636
+ continue;
637
+ }
638
+
639
+ const externalBridge = externalBridgeRegistry[action.externalBridgeId];
640
+ if (!externalBridge) {
641
+ this.logger.warn(
642
+ { actionId: action.id, bridgeId: action.externalBridgeId },
643
+ 'Bridge not found in registry',
644
+ );
645
+ continue;
646
+ }
647
+
648
+ try {
649
+ const status = await externalBridge.getStatus(
650
+ action.txHash,
651
+ action.origin,
652
+ action.destination,
653
+ );
654
+
655
+ if (status.status === 'complete') {
656
+ await this.completeRebalanceAction(action.id);
657
+ completed++;
658
+ this.logger.info(
659
+ {
660
+ actionId: action.id,
661
+ txHash: action.txHash,
662
+ receivedAmount: status.receivedAmount?.toString(),
663
+ },
664
+ 'Inventory movement completed',
665
+ );
666
+ } else if (status.status === 'failed') {
667
+ await this.failRebalanceAction(action.id);
668
+ failed++;
669
+ this.logger.warn(
670
+ {
671
+ actionId: action.id,
672
+ txHash: action.txHash,
673
+ error: status.error,
674
+ },
675
+ 'Inventory movement failed',
676
+ );
677
+ } else if (status.status === 'pending') {
678
+ this.logger.debug(
679
+ {
680
+ actionId: action.id,
681
+ txHash: action.txHash,
682
+ substatus: status.substatus,
683
+ },
684
+ 'Inventory movement still pending',
685
+ );
686
+ }
687
+ // status === 'not_found' - wait for next cycle
688
+ } catch (error) {
689
+ this.logger.debug(
690
+ {
691
+ actionId: action.id,
692
+ txHash: action.txHash,
693
+ error: (error as Error).message,
694
+ },
695
+ 'Failed to get inventory movement status',
696
+ );
697
+ }
698
+ }
699
+
700
+ if (inventoryMovements.length > 0) {
701
+ this.logger.info(
702
+ {
703
+ completed,
704
+ failed,
705
+ pending: inventoryMovements.length - completed - failed,
706
+ },
707
+ 'Inventory movements synced',
708
+ );
709
+ }
710
+
711
+ return { completed, failed };
452
712
  }
453
713
 
454
714
  // === Debug Helpers ===
@@ -609,7 +869,6 @@ export class ActionTracker implements IActionTracker {
609
869
  origin: msg.origin_domain_id,
610
870
  destination: msg.destination_domain_id,
611
871
  amount,
612
- fulfilledAmount: 0n,
613
872
  priority: undefined,
614
873
  strategyType: undefined,
615
874
  createdAt: Date.now(),
@@ -622,10 +881,11 @@ export class ActionTracker implements IActionTracker {
622
881
  'Created synthetic RebalanceIntent',
623
882
  );
624
883
 
625
- // Create action
884
+ // Create action (recovered actions are always rebalance_message type)
626
885
  const action: RebalanceAction = {
627
886
  id: msg.msg_id,
628
887
  status: 'in_progress',
888
+ type: 'rebalance_message',
629
889
  intentId: intent.id,
630
890
  messageId: msg.msg_id,
631
891
  txHash: msg.origin_tx_hash,
@@ -1,8 +1,17 @@
1
1
  import type { Address, Domain } from '@hyperlane-xyz/utils';
2
2
 
3
+ import type { ExternalBridgeType } from '../config/types.js';
4
+ import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
3
5
  import type { ConfirmedBlockTags } from '../interfaces/IMonitor.js';
4
6
 
5
- import type { RebalanceAction, RebalanceIntent, Transfer } from './types.js';
7
+ import type {
8
+ ActionType,
9
+ ExecutionMethod,
10
+ PartialInventoryIntent,
11
+ RebalanceAction,
12
+ RebalanceIntent,
13
+ Transfer,
14
+ } from './types.js';
6
15
 
7
16
  export interface CreateRebalanceIntentParams {
8
17
  origin: Domain;
@@ -11,6 +20,8 @@ export interface CreateRebalanceIntentParams {
11
20
  bridge?: Address;
12
21
  priority?: number;
13
22
  strategyType?: string;
23
+ executionMethod?: ExecutionMethod;
24
+ externalBridge?: ExternalBridgeType;
14
25
  }
15
26
 
16
27
  export interface CreateRebalanceActionParams {
@@ -18,8 +29,11 @@ export interface CreateRebalanceActionParams {
18
29
  origin: Domain;
19
30
  destination: Domain;
20
31
  amount: bigint;
21
- messageId: string;
32
+ type: ActionType; // Required - type of action being created
33
+ messageId?: string; // Optional - not needed for inventory_movement
22
34
  txHash?: string;
35
+ externalBridgeTransferId?: string; // Optional - for inventory_movement (external transfer bridge ID)
36
+ externalBridgeId?: ExternalBridgeType; // Optional - for inventory_movement (e.g., 'lifi')
23
37
  }
24
38
 
25
39
  /**
@@ -56,6 +70,18 @@ export interface IActionTracker {
56
70
  */
57
71
  syncRebalanceActions(confirmedBlockTags?: ConfirmedBlockTags): Promise<void>;
58
72
 
73
+ /**
74
+ * Sync inventory_movement actions by checking their status via external bridge API.
75
+ * This is separate from syncRebalanceActions because inventory_movement actions
76
+ * don't use Hyperlane messages and need to query the bridge's status API.
77
+ *
78
+ * @param externalBridgeRegistry - Bridge registry to query for status
79
+ * @returns Count of completed and failed actions
80
+ */
81
+ syncInventoryMovementActions(
82
+ externalBridgeRegistry: Partial<ExternalBridgeRegistry>,
83
+ ): Promise<{ completed: number; failed: number }>;
84
+
59
85
  // === Transfer Queries ===
60
86
 
61
87
  /**
@@ -92,6 +118,13 @@ export interface IActionTracker {
92
118
  destination: Domain,
93
119
  ): Promise<RebalanceIntent[]>;
94
120
 
121
+ /**
122
+ * Get inventory intents that are in_progress but not fully fulfilled,
123
+ * and have no in-flight actions (safe to continue).
124
+ * Returns enriched data with computed values derived from action states.
125
+ */
126
+ getPartiallyFulfilledInventoryIntents(): Promise<PartialInventoryIntent[]>;
127
+
95
128
  // === RebalanceIntent Management ===
96
129
 
97
130
  /**
@@ -121,6 +154,28 @@ export interface IActionTracker {
121
154
 
122
155
  // === RebalanceAction Queries ===
123
156
 
157
+ /**
158
+ * Get actions filtered by type.
159
+ * @param type - Action type to filter by
160
+ */
161
+ getActionsByType(type: ActionType): Promise<RebalanceAction[]>;
162
+
163
+ /**
164
+ * Get all actions associated with a specific intent.
165
+ * @param intentId - ID of the intent
166
+ */
167
+ getActionsForIntent(intentId: string): Promise<RebalanceAction[]>;
168
+
169
+ /**
170
+ * Get total inflight inventory movement amount from a specific chain.
171
+ * Returns the sum of amounts for all in_progress inventory_movement actions
172
+ * that originate from the specified domain.
173
+ *
174
+ * @param origin - Domain ID of the origin chain
175
+ * @returns Total amount being moved out via inventory movements
176
+ */
177
+ getInflightInventoryMovements(origin: Domain): Promise<bigint>;
178
+
124
179
  /**
125
180
  * Get a single rebalance action by ID.
126
181
  */
@@ -139,7 +194,7 @@ export interface IActionTracker {
139
194
 
140
195
  /**
141
196
  * Mark a rebalance action as complete.
142
- * Updates parent intent's fulfilledAmount.
197
+ * Checks if parent intent is now fully fulfilled and marks it complete if so.
143
198
  */
144
199
  completeRebalanceAction(id: string): Promise<void>;
145
200
 
@@ -16,6 +16,7 @@ describe('InflightContextAdapter', () => {
16
16
  actionTracker = {
17
17
  getActiveRebalanceIntents: Sinon.stub(),
18
18
  getInProgressTransfers: Sinon.stub(),
19
+ getActionsForIntent: Sinon.stub(),
19
20
  } as any;
20
21
 
21
22
  multiProvider = {
@@ -40,7 +41,6 @@ describe('InflightContextAdapter', () => {
40
41
  origin: 1,
41
42
  destination: 2,
42
43
  amount: 1000n,
43
- fulfilledAmount: 0n,
44
44
  status: 'not_started',
45
45
  createdAt: Date.now(),
46
46
  updatedAt: Date.now(),
@@ -64,6 +64,7 @@ describe('InflightContextAdapter', () => {
64
64
 
65
65
  actionTracker.getActiveRebalanceIntents.resolves(mockIntents);
66
66
  actionTracker.getInProgressTransfers.resolves(mockTransfers);
67
+ actionTracker.getActionsForIntent.resolves([]); // No actions
67
68
  multiProvider.getChainName.withArgs(1).returns('ethereum');
68
69
  multiProvider.getChainName.withArgs(2).returns('arbitrum');
69
70
 
@@ -74,6 +75,9 @@ describe('InflightContextAdapter', () => {
74
75
  origin: 'ethereum',
75
76
  destination: 'arbitrum',
76
77
  amount: 1000n,
78
+ deliveredAmount: 0n,
79
+ awaitingDeliveryAmount: 0n,
80
+ executionMethod: undefined,
77
81
  bridge: undefined,
78
82
  });
79
83
 
@@ -102,7 +106,6 @@ describe('InflightContextAdapter', () => {
102
106
  origin: 137,
103
107
  destination: 10,
104
108
  amount: 2000n,
105
- fulfilledAmount: 0n,
106
109
  status: 'not_started',
107
110
  createdAt: Date.now(),
108
111
  updatedAt: Date.now(),
@@ -126,6 +129,7 @@ describe('InflightContextAdapter', () => {
126
129
 
127
130
  actionTracker.getActiveRebalanceIntents.resolves(mockIntents);
128
131
  actionTracker.getInProgressTransfers.resolves(mockTransfers);
132
+ actionTracker.getActionsForIntent.resolves([]);
129
133
  multiProvider.getChainName.withArgs(137).returns('polygon');
130
134
  multiProvider.getChainName.withArgs(10).returns('optimism');
131
135
 
@@ -144,7 +148,6 @@ describe('InflightContextAdapter', () => {
144
148
  origin: 1,
145
149
  destination: 2,
146
150
  amount: 1000n,
147
- fulfilledAmount: 0n,
148
151
  status: 'not_started',
149
152
  createdAt: Date.now(),
150
153
  updatedAt: Date.now(),
@@ -154,7 +157,6 @@ describe('InflightContextAdapter', () => {
154
157
  origin: 2,
155
158
  destination: 3,
156
159
  amount: 1500n,
157
- fulfilledAmount: 0n,
158
160
  status: 'in_progress',
159
161
  createdAt: Date.now(),
160
162
  updatedAt: Date.now(),
@@ -190,6 +192,7 @@ describe('InflightContextAdapter', () => {
190
192
 
191
193
  actionTracker.getActiveRebalanceIntents.resolves(mockIntents);
192
194
  actionTracker.getInProgressTransfers.resolves(mockTransfers);
195
+ actionTracker.getActionsForIntent.resolves([]);
193
196
  multiProvider.getChainName.withArgs(1).returns('ethereum');
194
197
  multiProvider.getChainName.withArgs(2).returns('arbitrum');
195
198
  multiProvider.getChainName.withArgs(3).returns('optimism');
@@ -1,6 +1,9 @@
1
1
  import type { MultiProvider } from '@hyperlane-xyz/sdk';
2
2
 
3
- import type { InflightContext } from '../interfaces/IStrategy.js';
3
+ import type {
4
+ InflightContext,
5
+ RouteWithContext,
6
+ } from '../interfaces/IStrategy.js';
4
7
 
5
8
  import type { IActionTracker } from './IActionTracker.js';
6
9
 
@@ -22,14 +25,44 @@ export class InflightContextAdapter {
22
25
  const intents = await this.actionTracker.getActiveRebalanceIntents();
23
26
  const transfers = await this.actionTracker.getInProgressTransfers();
24
27
 
25
- const pendingRebalances = intents.map((intent) => ({
26
- origin: this.multiProvider.getChainName(intent.origin),
27
- destination: this.multiProvider.getChainName(intent.destination),
28
- // TODO: Review once inventory rebalancing is implemented and we expect
29
- // partially fulfilled intents. May need to use (amount - fulfilledAmount).
30
- amount: intent.amount,
31
- bridge: intent.bridge,
32
- }));
28
+ const pendingRebalances: RouteWithContext[] = await Promise.all(
29
+ intents.map(async (intent) => {
30
+ let deliveredAmount = 0n;
31
+ let awaitingDeliveryAmount = 0n;
32
+
33
+ // For inventory intents, compute delivered and awaiting amounts from actions
34
+ if (intent.executionMethod === 'inventory') {
35
+ const actions = await this.actionTracker.getActionsForIntent(
36
+ intent.id,
37
+ );
38
+
39
+ // Sum of complete inventory_deposit actions (message delivered)
40
+ deliveredAmount = actions
41
+ .filter(
42
+ (a) => a.type === 'inventory_deposit' && a.status === 'complete',
43
+ )
44
+ .reduce((sum, a) => sum + a.amount, 0n);
45
+
46
+ // Sum of in_progress inventory_deposit actions (tx confirmed, message pending)
47
+ awaitingDeliveryAmount = actions
48
+ .filter(
49
+ (a) =>
50
+ a.type === 'inventory_deposit' && a.status === 'in_progress',
51
+ )
52
+ .reduce((sum, a) => sum + a.amount, 0n);
53
+ }
54
+
55
+ return {
56
+ origin: this.multiProvider.getChainName(intent.origin),
57
+ destination: this.multiProvider.getChainName(intent.destination),
58
+ amount: intent.amount,
59
+ deliveredAmount,
60
+ awaitingDeliveryAmount,
61
+ executionMethod: intent.executionMethod,
62
+ bridge: intent.bridge,
63
+ };
64
+ }),
65
+ );
33
66
 
34
67
  const pendingTransfers = transfers.map((transfer) => ({
35
68
  origin: this.multiProvider.getChainName(transfer.origin),