@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
@@ -9,12 +9,15 @@ import {
9
9
  } from '@hyperlane-xyz/sdk';
10
10
  import type { Address } from '@hyperlane-xyz/utils';
11
11
 
12
+ import { ExternalBridgeType } from '../config/types.js';
12
13
  import type {
14
+ InventoryRoute,
13
15
  RawBalances,
14
16
  Route,
15
17
  StrategyRoute,
16
18
  } from '../interfaces/IStrategy.js';
17
19
  import { extractBridgeConfigs } from '../test/helpers.js';
20
+ import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
18
21
 
19
22
  import { CollateralDeficitStrategy } from './CollateralDeficitStrategy.js';
20
23
 
@@ -183,6 +186,7 @@ describe('CollateralDeficitStrategy', () => {
183
186
  origin: chain2,
184
187
  destination: chain1,
185
188
  amount: 5_000_000n, // 5 USDC pending to chain1
189
+ executionType: 'movableCollateral',
186
190
  bridge: BRIDGE2, // Matches chain2's bridge for chain2->chain1 route
187
191
  },
188
192
  ];
@@ -229,6 +233,7 @@ describe('CollateralDeficitStrategy', () => {
229
233
  origin: chain2,
230
234
  destination: chain1,
231
235
  amount: 5_000_000n,
236
+ executionType: 'movableCollateral',
232
237
  bridge: OTHER_BRIDGE, // Does NOT match chain2's configured bridge for chain2->chain1
233
238
  },
234
239
  ];
@@ -271,6 +276,7 @@ describe('CollateralDeficitStrategy', () => {
271
276
  origin: chain2,
272
277
  destination: chain1,
273
278
  amount: 10_000_000n, // 10 USDC pending - more than enough
279
+ executionType: 'movableCollateral',
274
280
  bridge: BRIDGE2, // Matches chain2's configured bridge for chain2->chain1
275
281
  },
276
282
  ];
@@ -383,7 +389,9 @@ describe('CollateralDeficitStrategy', () => {
383
389
  expect(routes).to.have.lengthOf(1);
384
390
  expect(routes[0].origin).to.equal(chain2);
385
391
  expect(routes[0].destination).to.equal(chain1);
386
- expect(routes[0].bridge).to.equal(BRIDGE2); // Uses chain2's (origin) bridge
392
+ if (routes[0].executionType === 'movableCollateral') {
393
+ expect(routes[0].bridge).to.equal(BRIDGE2); // Uses chain2's (origin) bridge
394
+ }
387
395
  });
388
396
 
389
397
  it('should generate routes from surplus to deficit chains', () => {
@@ -473,8 +481,11 @@ describe('CollateralDeficitStrategy', () => {
473
481
  const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
474
482
 
475
483
  expect(filtered).to.have.lengthOf(2);
476
- expect((filtered[0] as StrategyRoute).bridge).to.equal(BRIDGE2);
477
- expect((filtered[1] as StrategyRoute).bridge).to.be.undefined;
484
+ expect((filtered[0] as Route & { bridge?: Address }).bridge).to.equal(
485
+ BRIDGE2,
486
+ );
487
+ expect((filtered[1] as Route & { bridge?: Address }).bridge).to.be
488
+ .undefined;
478
489
  });
479
490
 
480
491
  it('should include rebalance when bridge matches configured bridge for the route', () => {
@@ -502,7 +513,9 @@ describe('CollateralDeficitStrategy', () => {
502
513
 
503
514
  const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
504
515
  expect(filtered).to.have.lengthOf(1);
505
- expect((filtered[0] as StrategyRoute).bridge).to.equal(BRIDGE2);
516
+ expect((filtered[0] as Route & { bridge?: Address }).bridge).to.equal(
517
+ BRIDGE2,
518
+ );
506
519
  });
507
520
 
508
521
  it('should exclude rebalance when bridge does not match configured bridge for the route', () => {
@@ -525,6 +538,7 @@ describe('CollateralDeficitStrategy', () => {
525
538
  origin: chain2,
526
539
  destination: chain1,
527
540
  amount: 5_000_000n,
541
+ executionType: 'movableCollateral',
528
542
  bridge: BRIDGE1, // Does NOT match configured bridge for chain2->chain1 (should be BRIDGE2)
529
543
  },
530
544
  ];
@@ -548,4 +562,167 @@ describe('CollateralDeficitStrategy', () => {
548
562
  expect(filtered).to.have.lengthOf(0);
549
563
  });
550
564
  });
565
+
566
+ describe('inventory execution type', () => {
567
+ it('should create inventory route when config is inventory type', () => {
568
+ const config = {
569
+ [chain1]: {
570
+ executionType: 'inventory',
571
+ externalBridge: ExternalBridgeType.LiFi,
572
+ buffer: '1000',
573
+ },
574
+ [chain2]: {
575
+ executionType: 'inventory',
576
+ externalBridge: ExternalBridgeType.LiFi,
577
+ buffer: '500',
578
+ },
579
+ };
580
+ const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {
581
+ [chain1]: {
582
+ executionType: 'inventory',
583
+ externalBridge: ExternalBridgeType.LiFi,
584
+ },
585
+ [chain2]: {
586
+ executionType: 'inventory',
587
+ externalBridge: ExternalBridgeType.LiFi,
588
+ },
589
+ };
590
+
591
+ const strategy = new CollateralDeficitStrategy(
592
+ config as any,
593
+ tokensByChainName,
594
+ testLogger,
595
+ bridgeConfigs,
596
+ );
597
+
598
+ // Start with positive balances
599
+ const rawBalances: RawBalances = {
600
+ [chain1]: 2_000_000n, // 2 USDC
601
+ [chain2]: 20_000_000n, // 20 USDC
602
+ };
603
+
604
+ // Pending transfer will drain chain1 to create deficit
605
+ const inflightContext = {
606
+ pendingTransfers: [
607
+ {
608
+ origin: chain2,
609
+ destination: chain1,
610
+ amount: 7_000_000n, // 7 USDC pending to chain1
611
+ },
612
+ ],
613
+ pendingRebalances: [] as StrategyRoute[],
614
+ };
615
+
616
+ // After reserveCollateral: chain1 = 2 - 7 = -5 USDC (deficit)
617
+ const routes = strategy.getRebalancingRoutes(
618
+ rawBalances,
619
+ inflightContext,
620
+ );
621
+
622
+ expect(routes).to.have.lengthOf(1);
623
+ expect(routes[0].origin).to.equal(chain2);
624
+ expect(routes[0].destination).to.equal(chain1);
625
+ expect(routes[0].executionType).to.equal('inventory');
626
+ expect((routes[0] as InventoryRoute).externalBridge).to.equal(
627
+ ExternalBridgeType.LiFi,
628
+ );
629
+ });
630
+
631
+ it('should include pending inventory rebalances with matching externalBridge in filter', () => {
632
+ const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {
633
+ [chain1]: {
634
+ executionType: 'inventory',
635
+ externalBridge: ExternalBridgeType.LiFi,
636
+ },
637
+ [chain2]: {
638
+ executionType: 'inventory',
639
+ externalBridge: ExternalBridgeType.LiFi,
640
+ },
641
+ };
642
+
643
+ const config = {
644
+ [chain1]: {
645
+ executionType: 'inventory',
646
+ externalBridge: ExternalBridgeType.LiFi,
647
+ buffer: '1000',
648
+ },
649
+ [chain2]: {
650
+ executionType: 'inventory',
651
+ externalBridge: ExternalBridgeType.LiFi,
652
+ buffer: '500',
653
+ },
654
+ };
655
+
656
+ const strategy = new CollateralDeficitStrategy(
657
+ config as any,
658
+ tokensByChainName,
659
+ testLogger,
660
+ bridgeConfigs,
661
+ );
662
+
663
+ const pendingRebalances: InventoryRoute[] = [
664
+ {
665
+ origin: chain2,
666
+ destination: chain1,
667
+ amount: 5_000_000n,
668
+ executionType: 'inventory',
669
+ externalBridge: ExternalBridgeType.LiFi,
670
+ },
671
+ ];
672
+
673
+ const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
674
+ expect(filtered).to.have.lengthOf(1);
675
+ expect((filtered[0] as InventoryRoute).externalBridge).to.equal(
676
+ ExternalBridgeType.LiFi,
677
+ );
678
+ });
679
+
680
+ it('should exclude pending inventory rebalances with non-matching externalBridge', () => {
681
+ const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {
682
+ [chain1]: {
683
+ executionType: 'inventory',
684
+ externalBridge: ExternalBridgeType.LiFi,
685
+ },
686
+ [chain2]: {
687
+ executionType: 'inventory',
688
+ externalBridge: ExternalBridgeType.LiFi,
689
+ },
690
+ };
691
+
692
+ const config = {
693
+ [chain1]: {
694
+ executionType: 'inventory',
695
+ externalBridge: ExternalBridgeType.LiFi,
696
+ buffer: '1000',
697
+ },
698
+ [chain2]: {
699
+ executionType: 'inventory',
700
+ externalBridge: ExternalBridgeType.LiFi,
701
+ buffer: '500',
702
+ },
703
+ };
704
+
705
+ const strategy = new CollateralDeficitStrategy(
706
+ config as any,
707
+ tokensByChainName,
708
+ testLogger,
709
+ bridgeConfigs,
710
+ );
711
+
712
+ // Create a route with a different externalBridge value to test mismatch
713
+ // Using type assertion since we're testing the filtering logic
714
+ const pendingRebalances = [
715
+ {
716
+ origin: chain2,
717
+ destination: chain1,
718
+ amount: 5_000_000n,
719
+ executionType: 'inventory' as const,
720
+ externalBridge: 'nonexistent_bridge' as ExternalBridgeType, // Different bridge
721
+ },
722
+ ];
723
+
724
+ const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
725
+ expect(filtered).to.have.lengthOf(0);
726
+ });
727
+ });
551
728
  });
@@ -14,7 +14,12 @@ import type {
14
14
  StrategyRoute,
15
15
  } from '../interfaces/IStrategy.js';
16
16
  import { Metrics } from '../metrics/Metrics.js';
17
- import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
17
+ import {
18
+ type BridgeConfigWithOverride,
19
+ createStrategyRoute,
20
+ isInventoryConfig,
21
+ isMovableCollateralConfig,
22
+ } from '../utils/bridgeUtils.js';
18
23
 
19
24
  import { BaseStrategy, type Delta } from './BaseStrategy.js';
20
25
 
@@ -249,12 +254,14 @@ export class CollateralDeficitStrategy extends BaseStrategy {
249
254
  surplus.chain,
250
255
  deficit.chain,
251
256
  );
252
- routes.push({
253
- origin: surplus.chain,
254
- destination: deficit.chain,
255
- amount: transferAmount,
256
- bridge: bridgeConfig.bridge,
257
- });
257
+ routes.push(
258
+ createStrategyRoute(
259
+ bridgeConfig,
260
+ surplus.chain,
261
+ deficit.chain,
262
+ transferAmount,
263
+ ),
264
+ );
258
265
  }
259
266
 
260
267
  deficit.amount -= transferAmount;
@@ -275,6 +282,11 @@ export class CollateralDeficitStrategy extends BaseStrategy {
275
282
 
276
283
  const filteredRoutes = this.filterRoutes(routes, actualBalances);
277
284
 
285
+ // Record metrics for each intent created
286
+ for (const route of filteredRoutes) {
287
+ this.metrics?.recordIntentCreated(route, this.name);
288
+ }
289
+
278
290
  this.logger.debug(
279
291
  {
280
292
  context: this.constructor.name,
@@ -299,18 +311,33 @@ export class CollateralDeficitStrategy extends BaseStrategy {
299
311
  }
300
312
 
301
313
  return pendingRebalances.filter((rebalance) => {
302
- if (!('bridge' in rebalance) || !rebalance.bridge) {
303
- this.logger.debug(
304
- { origin: rebalance.origin, destination: rebalance.destination },
305
- 'Including pending rebalance without bridge (recovered intent)',
306
- );
307
- return true;
308
- }
309
314
  const bridgeConfig = this.getBridgeConfigForRoute(
310
315
  rebalance.origin,
311
316
  rebalance.destination,
312
317
  );
313
- return bridgeConfig?.bridge === rebalance.bridge;
318
+
319
+ // For movableCollateral routes: match bridge address
320
+ if ('bridge' in rebalance && rebalance.bridge) {
321
+ return (
322
+ isMovableCollateralConfig(bridgeConfig) &&
323
+ bridgeConfig.bridge === rebalance.bridge
324
+ );
325
+ }
326
+
327
+ // For inventory routes: match externalBridge type
328
+ if ('externalBridge' in rebalance && rebalance.externalBridge) {
329
+ return (
330
+ isInventoryConfig(bridgeConfig) &&
331
+ bridgeConfig.externalBridge === rebalance.externalBridge
332
+ );
333
+ }
334
+
335
+ // Recovered intents without bridge info - include to be safe
336
+ this.logger.debug(
337
+ { origin: rebalance.origin, destination: rebalance.destination },
338
+ 'Including pending rebalance without bridge (recovered intent)',
339
+ );
340
+ return true;
314
341
  });
315
342
  }
316
343
 
@@ -75,12 +75,14 @@ describe('CompositeStrategy', () => {
75
75
  origin: chain1,
76
76
  destination: chain2,
77
77
  amount: 1000n,
78
+ executionType: 'movableCollateral',
78
79
  bridge: TEST_BRIDGE,
79
80
  };
80
81
  const route2: StrategyRoute = {
81
82
  origin: chain2,
82
83
  destination: chain3,
83
84
  amount: 2000n,
85
+ executionType: 'movableCollateral',
84
86
  bridge: TEST_BRIDGE,
85
87
  };
86
88
 
@@ -110,6 +112,7 @@ describe('CompositeStrategy', () => {
110
112
  origin: chain1,
111
113
  destination: chain2,
112
114
  amount: 1000n,
115
+ executionType: 'movableCollateral',
113
116
  bridge: TEST_BRIDGE,
114
117
  };
115
118
 
@@ -147,18 +150,21 @@ describe('CompositeStrategy', () => {
147
150
  origin: chain1,
148
151
  destination: chain2,
149
152
  amount: 1000n,
153
+ executionType: 'movableCollateral',
150
154
  bridge: TEST_BRIDGE,
151
155
  };
152
156
  const route2: StrategyRoute = {
153
157
  origin: chain2,
154
158
  destination: chain3,
155
159
  amount: 2000n,
160
+ executionType: 'movableCollateral',
156
161
  bridge: TEST_BRIDGE,
157
162
  };
158
163
  const route3: StrategyRoute = {
159
164
  origin: chain3,
160
165
  destination: chain1,
161
166
  amount: 3000n,
167
+ executionType: 'movableCollateral',
162
168
  bridge: TEST_BRIDGE,
163
169
  };
164
170
 
@@ -206,6 +212,7 @@ describe('CompositeStrategy', () => {
206
212
  origin: chain3,
207
213
  destination: chain1,
208
214
  amount: 500n,
215
+ executionType: 'movableCollateral',
209
216
  bridge: TEST_BRIDGE,
210
217
  };
211
218
 
@@ -213,6 +220,7 @@ describe('CompositeStrategy', () => {
213
220
  origin: chain1,
214
221
  destination: chain2,
215
222
  amount: 1000n,
223
+ executionType: 'movableCollateral',
216
224
  bridge: TEST_BRIDGE,
217
225
  };
218
226
 
@@ -306,18 +314,21 @@ describe('CompositeStrategy', () => {
306
314
  origin: chain1,
307
315
  destination: chain2,
308
316
  amount: 1000n,
317
+ executionType: 'movableCollateral',
309
318
  bridge: TEST_BRIDGE,
310
319
  };
311
320
  const route1b: StrategyRoute = {
312
321
  origin: chain1,
313
322
  destination: chain3,
314
323
  amount: 1500n,
324
+ executionType: 'movableCollateral',
315
325
  bridge: TEST_BRIDGE,
316
326
  };
317
327
  const route2a: StrategyRoute = {
318
328
  origin: chain2,
319
329
  destination: chain3,
320
330
  amount: 2000n,
331
+ executionType: 'movableCollateral',
321
332
  bridge: TEST_BRIDGE,
322
333
  };
323
334
 
@@ -348,6 +359,7 @@ describe('CompositeStrategy', () => {
348
359
  origin: chain2,
349
360
  destination: chain3,
350
361
  amount: 2000n,
362
+ executionType: 'movableCollateral',
351
363
  bridge: TEST_BRIDGE,
352
364
  };
353
365
 
@@ -377,6 +389,7 @@ describe('CompositeStrategy', () => {
377
389
  origin: chain1,
378
390
  destination: chain2,
379
391
  amount: 1000n,
392
+ executionType: 'movableCollateral',
380
393
  bridge: TEST_BRIDGE,
381
394
  };
382
395
 
@@ -410,6 +410,7 @@ describe('MinAmountStrategy', () => {
410
410
  destination: chain1,
411
411
  amount: BigInt(70e18),
412
412
  bridge: AddressZero,
413
+ executionType: 'movableCollateral',
413
414
  },
414
415
  ]);
415
416
  });
@@ -467,12 +468,14 @@ describe('MinAmountStrategy', () => {
467
468
  destination: chain1,
468
469
  amount: BigInt(50e18),
469
470
  bridge: AddressZero,
471
+ executionType: 'movableCollateral',
470
472
  },
471
473
  {
472
474
  origin: chain3,
473
475
  destination: chain2,
474
476
  amount: BigInt(25e18),
475
477
  bridge: AddressZero,
478
+ executionType: 'movableCollateral',
476
479
  },
477
480
  ]);
478
481
  });
@@ -721,6 +724,7 @@ describe('MinAmountStrategy', () => {
721
724
  destination: chain1,
722
725
  amount: 100n,
723
726
  bridge: AddressZero,
727
+ executionType: 'movableCollateral',
724
728
  },
725
729
  ]);
726
730
  });
@@ -3,12 +3,17 @@ import { type Logger } from 'pino';
3
3
  import { type ChainMap, type Token } from '@hyperlane-xyz/sdk';
4
4
 
5
5
  import {
6
+ ExecutionType,
6
7
  RebalancerStrategyOptions,
7
8
  type StrategyConfig,
8
9
  } from '../config/types.js';
9
10
  import { type IStrategy } from '../interfaces/IStrategy.js';
10
11
  import { type Metrics } from '../metrics/Metrics.js';
11
- import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
12
+ import type {
13
+ BridgeConfigWithOverride,
14
+ InventoryBridgeConfig,
15
+ MovableCollateralBridgeConfig,
16
+ } from '../utils/bridgeUtils.js';
12
17
 
13
18
  import { CollateralDeficitStrategy } from './CollateralDeficitStrategy.js';
14
19
  import { CompositeStrategy } from './CompositeStrategy.js';
@@ -26,6 +31,7 @@ export class StrategyFactory {
26
31
  * @param initialTotalCollateral The initial total collateral of the rebalancer
27
32
  * @param logger The logger to use for the strategy
28
33
  * @param metrics The metrics to use for the strategy
34
+ * @param minAmountsByChain Optional minimum amounts per chain for filtering routes
29
35
  * @returns A concrete strategy implementation
30
36
  */
31
37
  static createStrategy(
@@ -34,6 +40,7 @@ export class StrategyFactory {
34
40
  initialTotalCollateral: bigint,
35
41
  logger: Logger,
36
42
  metrics?: Metrics,
43
+ minAmountsByChain?: ChainMap<bigint>,
37
44
  ): IStrategy {
38
45
  if (strategyConfigs.length === 0) {
39
46
  throw new Error('At least one strategy must be configured');
@@ -47,6 +54,7 @@ export class StrategyFactory {
47
54
  initialTotalCollateral,
48
55
  logger,
49
56
  metrics,
57
+ minAmountsByChain,
50
58
  );
51
59
  }
52
60
 
@@ -58,6 +66,7 @@ export class StrategyFactory {
58
66
  initialTotalCollateral,
59
67
  logger,
60
68
  metrics,
69
+ minAmountsByChain,
61
70
  ),
62
71
  );
63
72
  return new CompositeStrategy(subStrategies, logger);
@@ -72,6 +81,7 @@ export class StrategyFactory {
72
81
  initialTotalCollateral: bigint,
73
82
  logger: Logger,
74
83
  metrics?: Metrics,
84
+ _minAmountsByChain?: ChainMap<bigint>,
75
85
  ): IStrategy {
76
86
  const bridgeConfigs = this.extractBridgeConfigs(strategyConfig);
77
87
 
@@ -116,13 +126,30 @@ export class StrategyFactory {
116
126
  const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {};
117
127
 
118
128
  for (const [chain, config] of Object.entries(strategyConfig.chains)) {
119
- bridgeConfigs[chain] = {
120
- bridge: config.bridge,
129
+ const baseConfig = {
121
130
  bridgeMinAcceptedAmount: config.bridgeMinAcceptedAmount ?? 0,
122
- override: config.override as ChainMap<
123
- Partial<{ bridge: string; bridgeMinAcceptedAmount: string | number }>
124
- >,
125
131
  };
132
+
133
+ const executionType =
134
+ config.executionType ?? ExecutionType.MovableCollateral;
135
+
136
+ if (executionType === ExecutionType.Inventory) {
137
+ bridgeConfigs[chain] = {
138
+ ...baseConfig,
139
+ executionType: 'inventory',
140
+ externalBridge: config.externalBridge!, // Validated by config schema
141
+ override: config.override as ChainMap<Partial<InventoryBridgeConfig>>,
142
+ };
143
+ } else {
144
+ bridgeConfigs[chain] = {
145
+ ...baseConfig,
146
+ executionType: 'movableCollateral',
147
+ bridge: config.bridge!, // Validated by config schema
148
+ override: config.override as ChainMap<
149
+ Partial<MovableCollateralBridgeConfig>
150
+ >,
151
+ };
152
+ }
126
153
  }
127
154
 
128
155
  return bridgeConfigs;
@@ -258,6 +258,7 @@ describe('WeightedStrategy', () => {
258
258
  destination: chain1,
259
259
  amount: ethers.utils.parseEther('50').toBigInt(),
260
260
  bridge: ethers.constants.AddressZero,
261
+ executionType: 'movableCollateral',
261
262
  },
262
263
  ]);
263
264
  });
@@ -325,6 +326,7 @@ describe('WeightedStrategy', () => {
325
326
  destination: chain1,
326
327
  amount: ethers.utils.parseEther('100').toBigInt(),
327
328
  bridge: ethers.constants.AddressZero,
329
+ executionType: 'movableCollateral',
328
330
  },
329
331
  ]);
330
332
  });
@@ -363,12 +365,14 @@ describe('WeightedStrategy', () => {
363
365
  destination: chain1,
364
366
  amount: 133333333333333333333n,
365
367
  bridge: ethers.constants.AddressZero,
368
+ executionType: 'movableCollateral',
366
369
  },
367
370
  {
368
371
  origin: chain3,
369
372
  destination: chain2,
370
373
  amount: 133333333333333333333n,
371
374
  bridge: ethers.constants.AddressZero,
375
+ executionType: 'movableCollateral',
372
376
  },
373
377
  ]);
374
378
  });
@@ -408,12 +412,14 @@ describe('WeightedStrategy', () => {
408
412
  destination: chain1,
409
413
  amount: ethers.utils.parseEther('25').toBigInt(),
410
414
  bridge: ethers.constants.AddressZero,
415
+ executionType: 'movableCollateral',
411
416
  },
412
417
  {
413
418
  origin: chain3,
414
419
  destination: chain1,
415
420
  amount: ethers.utils.parseEther('25').toBigInt(),
416
421
  bridge: ethers.constants.AddressZero,
422
+ executionType: 'movableCollateral',
417
423
  },
418
424
  ]);
419
425
  });