@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
@@ -11,6 +11,7 @@ import {
11
11
 
12
12
  import { RebalancerMinAmountType } from '../config/types.js';
13
13
  import type { RawBalances } from '../interfaces/IStrategy.js';
14
+ import { extractBridgeConfigs } from '../test/helpers.js';
14
15
 
15
16
  import { MinAmountStrategy } from './MinAmountStrategy.js';
16
17
 
@@ -58,6 +59,7 @@ describe('MinAmountStrategy', () => {
58
59
  tokensByChainName,
59
60
  totalCollateral,
60
61
  testLogger,
62
+ {},
61
63
  ),
62
64
  ).to.throw('At least two chains must be configured');
63
65
  });
@@ -87,6 +89,7 @@ describe('MinAmountStrategy', () => {
87
89
  tokensByChainName,
88
90
  totalCollateral,
89
91
  testLogger,
92
+ {},
90
93
  );
91
94
  });
92
95
 
@@ -115,6 +118,7 @@ describe('MinAmountStrategy', () => {
115
118
  tokensByChainName,
116
119
  totalCollateral,
117
120
  testLogger,
121
+ {},
118
122
  );
119
123
  });
120
124
 
@@ -145,6 +149,7 @@ describe('MinAmountStrategy', () => {
145
149
  tokensByChainName,
146
150
  totalCollateral,
147
151
  testLogger,
152
+ {},
148
153
  ),
149
154
  ).to.throw('Minimum amount (-10) cannot be negative for chain chain2');
150
155
  });
@@ -176,6 +181,7 @@ describe('MinAmountStrategy', () => {
176
181
  tokensByChainName,
177
182
  totalCollateral,
178
183
  testLogger,
184
+ {},
179
185
  ),
180
186
  ).to.throw(
181
187
  'Target (80) must be greater than or equal to min (100) for chain chain1',
@@ -209,6 +215,7 @@ describe('MinAmountStrategy', () => {
209
215
  tokensByChainName,
210
216
  totalCollateral,
211
217
  testLogger,
218
+ {},
212
219
  ),
213
220
  ).to.throw(
214
221
  'Target (0.4) must be greater than or equal to min (0.5) for chain chain1',
@@ -241,6 +248,7 @@ describe('MinAmountStrategy', () => {
241
248
  tokensByChainName,
242
249
  totalCollateral,
243
250
  testLogger,
251
+ {},
244
252
  ).getRebalancingRoutes({
245
253
  [chain1]: 100n,
246
254
  [chain2]: 200n,
@@ -275,6 +283,7 @@ describe('MinAmountStrategy', () => {
275
283
  tokensByChainName,
276
284
  totalCollateral,
277
285
  testLogger,
286
+ {},
278
287
  ).getRebalancingRoutes({
279
288
  [chain1]: 100n,
280
289
  [chain3]: 300n,
@@ -308,6 +317,7 @@ describe('MinAmountStrategy', () => {
308
317
  tokensByChainName,
309
318
  totalCollateral,
310
319
  testLogger,
320
+ {},
311
321
  ).getRebalancingRoutes({
312
322
  [chain1]: 100n,
313
323
  [chain2]: -2n,
@@ -342,6 +352,7 @@ describe('MinAmountStrategy', () => {
342
352
  tokensByChainName,
343
353
  totalCollateral,
344
354
  testLogger,
355
+ {},
345
356
  );
346
357
 
347
358
  const rawBalances: RawBalances = {
@@ -355,30 +366,33 @@ describe('MinAmountStrategy', () => {
355
366
  });
356
367
 
357
368
  it('should return a single route when a chain is below minimum amount', () => {
358
- const strategy = new MinAmountStrategy(
359
- {
360
- [chain1]: {
361
- minAmount: {
362
- min: '100',
363
- target: '120',
364
- type: RebalancerMinAmountType.Absolute,
365
- },
366
- bridge: AddressZero,
367
- bridgeLockTime: 1,
369
+ const config = {
370
+ [chain1]: {
371
+ minAmount: {
372
+ min: '100',
373
+ target: '120',
374
+ type: RebalancerMinAmountType.Absolute,
368
375
  },
369
- [chain2]: {
370
- minAmount: {
371
- min: '100',
372
- target: '120',
373
- type: RebalancerMinAmountType.Absolute,
374
- },
375
- bridge: AddressZero,
376
- bridgeLockTime: 1,
376
+ bridge: AddressZero,
377
+ bridgeLockTime: 1,
378
+ },
379
+ [chain2]: {
380
+ minAmount: {
381
+ min: '100',
382
+ target: '120',
383
+ type: RebalancerMinAmountType.Absolute,
377
384
  },
385
+ bridge: AddressZero,
386
+ bridgeLockTime: 1,
378
387
  },
388
+ };
389
+ const bridgeConfigs = extractBridgeConfigs(config);
390
+ const strategy = new MinAmountStrategy(
391
+ config,
379
392
  tokensByChainName,
380
393
  totalCollateral,
381
394
  testLogger,
395
+ bridgeConfigs,
382
396
  );
383
397
 
384
398
  const rawBalances = {
@@ -393,44 +407,48 @@ describe('MinAmountStrategy', () => {
393
407
  origin: chain2,
394
408
  destination: chain1,
395
409
  amount: BigInt(70e18),
410
+ bridge: AddressZero,
396
411
  },
397
412
  ]);
398
413
  });
399
414
 
400
415
  it('should return multiple routes for multiple chains below minimum amount', () => {
401
- const strategy = new MinAmountStrategy(
402
- {
403
- [chain1]: {
404
- minAmount: {
405
- min: '80',
406
- target: '100',
407
- type: RebalancerMinAmountType.Absolute,
408
- },
409
- bridge: AddressZero,
410
- bridgeLockTime: 1,
416
+ const config = {
417
+ [chain1]: {
418
+ minAmount: {
419
+ min: '80',
420
+ target: '100',
421
+ type: RebalancerMinAmountType.Absolute,
411
422
  },
412
- [chain2]: {
413
- minAmount: {
414
- min: '80',
415
- target: '100',
416
- type: RebalancerMinAmountType.Absolute,
417
- },
418
- bridge: AddressZero,
419
- bridgeLockTime: 1,
423
+ bridge: AddressZero,
424
+ bridgeLockTime: 1,
425
+ },
426
+ [chain2]: {
427
+ minAmount: {
428
+ min: '80',
429
+ target: '100',
430
+ type: RebalancerMinAmountType.Absolute,
420
431
  },
421
- [chain3]: {
422
- minAmount: {
423
- min: '80',
424
- target: '100',
425
- type: RebalancerMinAmountType.Absolute,
426
- },
427
- bridge: AddressZero,
428
- bridgeLockTime: 1,
432
+ bridge: AddressZero,
433
+ bridgeLockTime: 1,
434
+ },
435
+ [chain3]: {
436
+ minAmount: {
437
+ min: '80',
438
+ target: '100',
439
+ type: RebalancerMinAmountType.Absolute,
429
440
  },
441
+ bridge: AddressZero,
442
+ bridgeLockTime: 1,
430
443
  },
444
+ };
445
+ const bridgeConfigs = extractBridgeConfigs(config);
446
+ const strategy = new MinAmountStrategy(
447
+ config,
431
448
  tokensByChainName,
432
449
  totalCollateral,
433
450
  testLogger,
451
+ bridgeConfigs,
434
452
  );
435
453
 
436
454
  const rawBalances = {
@@ -446,11 +464,13 @@ describe('MinAmountStrategy', () => {
446
464
  origin: chain3,
447
465
  destination: chain1,
448
466
  amount: BigInt(50e18),
467
+ bridge: AddressZero,
449
468
  },
450
469
  {
451
470
  origin: chain3,
452
471
  destination: chain2,
453
472
  amount: BigInt(25e18),
473
+ bridge: AddressZero,
454
474
  },
455
475
  ]);
456
476
  });
@@ -482,6 +502,7 @@ describe('MinAmountStrategy', () => {
482
502
  tokensByChainName,
483
503
  totalCollateral,
484
504
  testLogger,
505
+ {},
485
506
  ),
486
507
  ).to.throw(
487
508
  `Consider reducing the targets as the sum (340) is greater than sum of collaterals (300)`,
@@ -489,39 +510,42 @@ describe('MinAmountStrategy', () => {
489
510
  });
490
511
 
491
512
  it('should handle case where there is not enough surplus to meet all minimum requirements by scaling down deficits', () => {
492
- const strategy = new MinAmountStrategy(
493
- {
494
- [chain1]: {
495
- minAmount: {
496
- min: '100',
497
- target: '100',
498
- type: RebalancerMinAmountType.Absolute,
499
- },
500
- bridge: AddressZero,
501
- bridgeLockTime: 1,
513
+ const config = {
514
+ [chain1]: {
515
+ minAmount: {
516
+ min: '100',
517
+ target: '100',
518
+ type: RebalancerMinAmountType.Absolute,
502
519
  },
503
- [chain2]: {
504
- minAmount: {
505
- min: '100',
506
- target: '100',
507
- type: RebalancerMinAmountType.Absolute,
508
- },
509
- bridge: AddressZero,
510
- bridgeLockTime: 1,
520
+ bridge: AddressZero,
521
+ bridgeLockTime: 1,
522
+ },
523
+ [chain2]: {
524
+ minAmount: {
525
+ min: '100',
526
+ target: '100',
527
+ type: RebalancerMinAmountType.Absolute,
511
528
  },
512
- [chain3]: {
513
- minAmount: {
514
- min: '100',
515
- target: '100',
516
- type: RebalancerMinAmountType.Absolute,
517
- },
518
- bridge: AddressZero,
519
- bridgeLockTime: 1,
529
+ bridge: AddressZero,
530
+ bridgeLockTime: 1,
531
+ },
532
+ [chain3]: {
533
+ minAmount: {
534
+ min: '100',
535
+ target: '100',
536
+ type: RebalancerMinAmountType.Absolute,
520
537
  },
538
+ bridge: AddressZero,
539
+ bridgeLockTime: 1,
521
540
  },
541
+ };
542
+ const bridgeConfigs = extractBridgeConfigs(config);
543
+ const strategy = new MinAmountStrategy(
544
+ config,
522
545
  tokensByChainName,
523
546
  totalCollateral,
524
547
  testLogger,
548
+ bridgeConfigs,
525
549
  );
526
550
 
527
551
  const rawBalances = {
@@ -542,6 +566,78 @@ describe('MinAmountStrategy', () => {
542
566
  expect(routes[1].amount).to.equal(BigInt(25e18));
543
567
  });
544
568
 
569
+ it('should not produce zero-amount routes when scaling causes amounts to round to zero', () => {
570
+ // This test ensures that when deficit scaling produces zero amounts (due to integer division),
571
+ // these zero-amount routes are NOT included in the output.
572
+ // Bug scenario: totalSurplus=1, totalDeficit=3 -> each deficit of 1 scales to (1*1)/3 = 0
573
+ const config = {
574
+ [chain1]: {
575
+ minAmount: {
576
+ min: '90',
577
+ target: '100',
578
+ type: RebalancerMinAmountType.Absolute,
579
+ },
580
+ bridge: AddressZero,
581
+ bridgeLockTime: 1,
582
+ },
583
+ [chain2]: {
584
+ minAmount: {
585
+ min: '90',
586
+ target: '100',
587
+ type: RebalancerMinAmountType.Absolute,
588
+ },
589
+ bridge: AddressZero,
590
+ bridgeLockTime: 1,
591
+ },
592
+ [chain3]: {
593
+ minAmount: {
594
+ min: '90',
595
+ target: '100',
596
+ type: RebalancerMinAmountType.Absolute,
597
+ },
598
+ bridge: AddressZero,
599
+ bridgeLockTime: 1,
600
+ },
601
+ };
602
+ const bridgeConfigs = extractBridgeConfigs(config);
603
+ const strategy = new MinAmountStrategy(
604
+ config,
605
+ tokensByChainName,
606
+ totalCollateral,
607
+ testLogger,
608
+ bridgeConfigs,
609
+ );
610
+
611
+ // chain1 and chain2 are at 1 token each (far below min 90)
612
+ // chain3 has 91 tokens (surplus of 1 above min)
613
+ // Deficits: chain1 needs 99 (100-1), chain2 needs 99 (100-1), total = 198
614
+ // Surplus: chain3 has 1 (91-90)
615
+ // Scaling: each deficit of 99 becomes (99 * 1) / 198 = 0 (integer division!)
616
+ const rawBalances = {
617
+ [chain1]: BigInt(1e18),
618
+ [chain2]: BigInt(1e18),
619
+ [chain3]: BigInt(91e18),
620
+ };
621
+
622
+ const routes = strategy.getRebalancingRoutes(rawBalances);
623
+
624
+ // All routes should have non-zero amounts
625
+ // (Chai's greaterThan doesn't support BigInt, so use direct comparison)
626
+ for (const route of routes) {
627
+ expect(
628
+ route.amount > 0n,
629
+ `Route amount should be > 0, got ${route.amount}`,
630
+ ).to.be.true;
631
+ }
632
+
633
+ // The single token of surplus may produce one route (or none if both scale to 0)
634
+ // Either way, no zero-amount routes should exist
635
+ expect(
636
+ routes.every((r) => r.amount > 0n),
637
+ 'All routes must have non-zero amounts',
638
+ ).to.be.true;
639
+ });
640
+
545
641
  it('should have no surplus or deficit when all at min', () => {
546
642
  const strategy = new MinAmountStrategy(
547
643
  {
@@ -567,6 +663,7 @@ describe('MinAmountStrategy', () => {
567
663
  tokensByChainName,
568
664
  totalCollateral,
569
665
  testLogger,
666
+ {},
570
667
  );
571
668
 
572
669
  const rawBalances = {
@@ -580,30 +677,33 @@ describe('MinAmountStrategy', () => {
580
677
  });
581
678
 
582
679
  it('should consider the target amount with relative configuration', () => {
583
- const strategy = new MinAmountStrategy(
584
- {
585
- [chain1]: {
586
- minAmount: {
587
- min: 0.25,
588
- target: 0.3,
589
- type: RebalancerMinAmountType.Relative,
590
- },
591
- bridge: AddressZero,
592
- bridgeLockTime: 1,
680
+ const config = {
681
+ [chain1]: {
682
+ minAmount: {
683
+ min: 0.25,
684
+ target: 0.3,
685
+ type: RebalancerMinAmountType.Relative,
593
686
  },
594
- [chain2]: {
595
- minAmount: {
596
- min: 0.25,
597
- target: 0.3,
598
- type: RebalancerMinAmountType.Relative,
599
- },
600
- bridge: AddressZero,
601
- bridgeLockTime: 1,
687
+ bridge: AddressZero,
688
+ bridgeLockTime: 1,
689
+ },
690
+ [chain2]: {
691
+ minAmount: {
692
+ min: 0.25,
693
+ target: 0.3,
694
+ type: RebalancerMinAmountType.Relative,
602
695
  },
696
+ bridge: AddressZero,
697
+ bridgeLockTime: 1,
603
698
  },
699
+ };
700
+ const bridgeConfigs = extractBridgeConfigs(config);
701
+ const strategy = new MinAmountStrategy(
702
+ config,
604
703
  tokensByChainName,
605
704
  totalCollateral,
606
705
  testLogger,
706
+ bridgeConfigs,
607
707
  );
608
708
 
609
709
  const rawBalances: RawBalances = {
@@ -618,6 +718,7 @@ describe('MinAmountStrategy', () => {
618
718
  origin: chain2,
619
719
  destination: chain1,
620
720
  amount: 100n,
721
+ bridge: AddressZero,
621
722
  },
622
723
  ]);
623
724
  });
@@ -7,9 +7,15 @@ import { fromWei, toWei } from '@hyperlane-xyz/utils';
7
7
  import {
8
8
  type MinAmountStrategyConfig,
9
9
  RebalancerMinAmountType,
10
+ RebalancerStrategyOptions,
10
11
  } from '../config/types.js';
11
- import type { RawBalances } from '../interfaces/IStrategy.js';
12
+ import type {
13
+ RawBalances,
14
+ Route,
15
+ StrategyRoute,
16
+ } from '../interfaces/IStrategy.js';
12
17
  import { type Metrics } from '../metrics/Metrics.js';
18
+ import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
13
19
 
14
20
  import { BaseStrategy, type Delta } from './BaseStrategy.js';
15
21
 
@@ -18,19 +24,21 @@ import { BaseStrategy, type Delta } from './BaseStrategy.js';
18
24
  * It ensures each chain has at least the specified minimum amount
19
25
  */
20
26
  export class MinAmountStrategy extends BaseStrategy {
27
+ readonly name = RebalancerStrategyOptions.MinAmount;
21
28
  private readonly config: MinAmountStrategyConfig = {};
22
29
  protected readonly logger: Logger;
23
30
 
24
31
  constructor(
25
32
  config: MinAmountStrategyConfig,
26
- private readonly tokensByChainName: ChainMap<Token>,
33
+ tokensByChainName: ChainMap<Token>,
27
34
  initialTotalCollateral: bigint,
28
35
  logger: Logger,
36
+ bridgeConfigs: ChainMap<BridgeConfigWithOverride>,
29
37
  metrics?: Metrics,
30
38
  ) {
31
39
  const chains = Object.keys(config);
32
40
  const log = logger.child({ class: MinAmountStrategy.name });
33
- super(chains, log, metrics);
41
+ super(chains, log, bridgeConfigs, metrics, tokensByChainName);
34
42
  this.logger = log;
35
43
 
36
44
  const minAmountType = config[chains[0]].minAmount.type;
@@ -67,37 +75,60 @@ export class MinAmountStrategy extends BaseStrategy {
67
75
  * Gets balances categorized by surplus and deficit based on minimum amounts and targets
68
76
  * - For absolute values: Uses exact token amounts
69
77
  * - For relative values: Uses percentages of total balance across all chains
78
+ *
79
+ * Simulates both types of rebalances before calculating surpluses/deficits:
80
+ * - pendingRebalances: in-flight intents (origin tx confirmed, add to destination only)
81
+ * - proposedRebalances: routes from earlier strategies (subtract from origin AND add to destination)
82
+ *
83
+ * This prevents over-rebalancing when multiple strategies run in sequence.
70
84
  */
71
- protected getCategorizedBalances(rawBalances: RawBalances): {
85
+ protected getCategorizedBalances(
86
+ rawBalances: RawBalances,
87
+ pendingRebalances?: Route[],
88
+ proposedRebalances?: StrategyRoute[],
89
+ ): {
72
90
  surpluses: Delta[];
73
91
  deficits: Delta[];
74
92
  } {
93
+ // Step 1: Simulate pending rebalances (in-flight, origin already deducted on-chain)
94
+ let simulatedBalances = this.simulatePendingRebalances(
95
+ rawBalances,
96
+ pendingRebalances ?? [],
97
+ );
98
+
99
+ // Step 2: Simulate proposed rebalances (from earlier strategies, not yet executed)
100
+ simulatedBalances = this.simulateProposedRebalances(
101
+ simulatedBalances,
102
+ proposedRebalances ?? [],
103
+ );
75
104
  const totalCollateral = this.chains.reduce(
76
- (sum, chain) => sum + rawBalances[chain],
105
+ (sum, chain) => sum + simulatedBalances[chain],
77
106
  0n,
78
107
  );
79
108
 
80
109
  return this.chains.reduce(
81
110
  (acc, chain) => {
82
- const config = this.config[chain];
83
- const balance = rawBalances[chain];
111
+ const chainConfig = this.config[chain];
112
+ const balance = simulatedBalances[chain];
84
113
  let minAmount: bigint;
85
114
  let targetAmount: bigint;
86
115
 
87
- if (config.minAmount.type === RebalancerMinAmountType.Absolute) {
116
+ if (chainConfig.minAmount.type === RebalancerMinAmountType.Absolute) {
88
117
  const token = this.getTokenByChainName(chain);
89
118
 
90
- minAmount = BigInt(toWei(config.minAmount.min, token.decimals));
91
- targetAmount = BigInt(toWei(config.minAmount.target, token.decimals));
119
+ minAmount = BigInt(toWei(chainConfig.minAmount.min, token.decimals));
120
+ targetAmount = BigInt(
121
+ toWei(chainConfig.minAmount.target, token.decimals),
122
+ );
92
123
  } else {
93
124
  minAmount = BigInt(
94
125
  BigNumber(totalCollateral.toString())
95
- .times(config.minAmount.min)
126
+ .times(chainConfig.minAmount.min)
96
127
  .toFixed(0, BigNumber.ROUND_FLOOR),
97
128
  );
98
129
  targetAmount = BigInt(
99
130
  BigNumber(totalCollateral.toString())
100
- .times(config.minAmount.target)
131
+ .times(chainConfig.minAmount.target)
101
132
  .toFixed(0, BigNumber.ROUND_FLOOR),
102
133
  );
103
134
  }
@@ -123,7 +154,7 @@ export class MinAmountStrategy extends BaseStrategy {
123
154
  }
124
155
 
125
156
  protected getTokenByChainName(chainName: string): Token {
126
- const token = this.tokensByChainName[chainName];
157
+ const token = this.tokensByChainName![chainName];
127
158
 
128
159
  if (token === undefined) {
129
160
  throw new Error(`Token not found for chain ${chainName}`);
@@ -61,7 +61,7 @@ describe('StrategyFactory', () => {
61
61
  };
62
62
 
63
63
  const strategy = StrategyFactory.createStrategy(
64
- strategyConfig,
64
+ [strategyConfig],
65
65
  tokensByChainName,
66
66
  totalCollateral,
67
67
  testLogger,
@@ -97,7 +97,7 @@ describe('StrategyFactory', () => {
97
97
  };
98
98
 
99
99
  const strategy = StrategyFactory.createStrategy(
100
- strategyConfig,
100
+ [strategyConfig],
101
101
  tokensByChainName,
102
102
  totalCollateral,
103
103
  testLogger,