@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
@@ -8,40 +8,123 @@ import {
8
8
  } from '../config/types.js';
9
9
  import { type IStrategy } from '../interfaces/IStrategy.js';
10
10
  import { type Metrics } from '../metrics/Metrics.js';
11
+ import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
11
12
 
13
+ import { CollateralDeficitStrategy } from './CollateralDeficitStrategy.js';
14
+ import { CompositeStrategy } from './CompositeStrategy.js';
12
15
  import { MinAmountStrategy } from './MinAmountStrategy.js';
13
16
  import { WeightedStrategy } from './WeightedStrategy.js';
14
17
 
15
18
  export class StrategyFactory {
16
19
  /**
17
- * @param strategyConfig A discriminated union of strategy-specific configurations.
18
- * @param tokensByChainName - A map of chain->token to ease the lookup of token by chain
19
- * @param initialTotalCollateral - The initial total collateral of the rebalancer
20
- * @param logger - The logger to use for the strategy
21
- * @param metrics - The metrics to use for the strategy
20
+ * Creates a strategy from an array of strategy configs.
21
+ * - Single strategy (array with 1 element): Creates that strategy directly
22
+ * - Multiple strategies (array with 2+ elements): Creates CompositeStrategy
23
+ *
24
+ * @param strategyConfigs Array of strategy configurations (always array format)
25
+ * @param tokensByChainName A map of chain->token to ease the lookup of token by chain
26
+ * @param initialTotalCollateral The initial total collateral of the rebalancer
27
+ * @param logger The logger to use for the strategy
28
+ * @param metrics The metrics to use for the strategy
22
29
  * @returns A concrete strategy implementation
23
30
  */
24
31
  static createStrategy(
32
+ strategyConfigs: StrategyConfig[],
33
+ tokensByChainName: ChainMap<Token>,
34
+ initialTotalCollateral: bigint,
35
+ logger: Logger,
36
+ metrics?: Metrics,
37
+ ): IStrategy {
38
+ if (strategyConfigs.length === 0) {
39
+ throw new Error('At least one strategy must be configured');
40
+ }
41
+
42
+ // Single strategy - create directly without CompositeStrategy wrapper
43
+ if (strategyConfigs.length === 1) {
44
+ return this.createSingleStrategy(
45
+ strategyConfigs[0],
46
+ tokensByChainName,
47
+ initialTotalCollateral,
48
+ logger,
49
+ metrics,
50
+ );
51
+ }
52
+
53
+ // Multiple strategies - create CompositeStrategy
54
+ const subStrategies = strategyConfigs.map((config) =>
55
+ this.createSingleStrategy(
56
+ config,
57
+ tokensByChainName,
58
+ initialTotalCollateral,
59
+ logger,
60
+ metrics,
61
+ ),
62
+ );
63
+ return new CompositeStrategy(subStrategies, logger);
64
+ }
65
+
66
+ /**
67
+ * Create a single strategy from config.
68
+ */
69
+ private static createSingleStrategy(
25
70
  strategyConfig: StrategyConfig,
26
71
  tokensByChainName: ChainMap<Token>,
27
72
  initialTotalCollateral: bigint,
28
73
  logger: Logger,
29
74
  metrics?: Metrics,
30
75
  ): IStrategy {
76
+ const bridgeConfigs = this.extractBridgeConfigs(strategyConfig);
77
+
31
78
  switch (strategyConfig.rebalanceStrategy) {
32
- case RebalancerStrategyOptions.Weighted:
33
- return new WeightedStrategy(strategyConfig.chains, logger, metrics);
34
- case RebalancerStrategyOptions.MinAmount:
79
+ case RebalancerStrategyOptions.Weighted: {
80
+ return new WeightedStrategy(
81
+ strategyConfig.chains,
82
+ logger,
83
+ bridgeConfigs,
84
+ metrics,
85
+ tokensByChainName,
86
+ );
87
+ }
88
+ case RebalancerStrategyOptions.MinAmount: {
35
89
  return new MinAmountStrategy(
36
90
  strategyConfig.chains,
37
91
  tokensByChainName,
38
92
  initialTotalCollateral,
39
93
  logger,
94
+ bridgeConfigs,
95
+ metrics,
96
+ );
97
+ }
98
+ case RebalancerStrategyOptions.CollateralDeficit: {
99
+ return new CollateralDeficitStrategy(
100
+ strategyConfig.chains,
101
+ tokensByChainName,
102
+ logger,
103
+ bridgeConfigs,
40
104
  metrics,
41
105
  );
106
+ }
42
107
  default: {
43
108
  throw new Error('Unsupported strategy type');
44
109
  }
45
110
  }
46
111
  }
112
+
113
+ private static extractBridgeConfigs(
114
+ strategyConfig: StrategyConfig,
115
+ ): ChainMap<BridgeConfigWithOverride> {
116
+ const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {};
117
+
118
+ for (const [chain, config] of Object.entries(strategyConfig.chains)) {
119
+ bridgeConfigs[chain] = {
120
+ bridge: config.bridge,
121
+ bridgeMinAcceptedAmount: config.bridgeMinAcceptedAmount ?? 0,
122
+ override: config.override as ChainMap<
123
+ Partial<{ bridge: string; bridgeMinAcceptedAmount: string | number }>
124
+ >,
125
+ };
126
+ }
127
+
128
+ return bridgeConfigs;
129
+ }
47
130
  }
@@ -2,9 +2,10 @@ import { expect } from 'chai';
2
2
  import { ethers } from 'ethers';
3
3
  import { pino } from 'pino';
4
4
 
5
- import type { ChainName } from '@hyperlane-xyz/sdk';
5
+ import type { ChainMap, ChainName, Token } from '@hyperlane-xyz/sdk';
6
6
 
7
7
  import type { RawBalances } from '../interfaces/IStrategy.js';
8
+ import { extractBridgeConfigs } from '../test/helpers.js';
8
9
 
9
10
  import { WeightedStrategy } from './WeightedStrategy.js';
10
11
 
@@ -34,6 +35,7 @@ describe('WeightedStrategy', () => {
34
35
  },
35
36
  },
36
37
  testLogger,
38
+ {},
37
39
  ),
38
40
  ).to.throw('At least two chains must be configured');
39
41
  });
@@ -55,6 +57,7 @@ describe('WeightedStrategy', () => {
55
57
  },
56
58
  },
57
59
  testLogger,
60
+ {},
58
61
  ),
59
62
  ).to.throw('Weight (-1) must not be negative for chain2');
60
63
  });
@@ -76,6 +79,7 @@ describe('WeightedStrategy', () => {
76
79
  },
77
80
  },
78
81
  testLogger,
82
+ {},
79
83
  ),
80
84
  ).to.throw('The total weight for all chains must be greater than 0');
81
85
  });
@@ -97,6 +101,7 @@ describe('WeightedStrategy', () => {
97
101
  },
98
102
  },
99
103
  testLogger,
104
+ {},
100
105
  ),
101
106
  ).to.throw('Tolerance (-1) must be between 0 and 100 for chain2');
102
107
 
@@ -116,6 +121,7 @@ describe('WeightedStrategy', () => {
116
121
  },
117
122
  },
118
123
  testLogger,
124
+ {},
119
125
  ),
120
126
  ).to.throw('Tolerance (101) must be between 0 and 100 for chain2');
121
127
  });
@@ -138,6 +144,7 @@ describe('WeightedStrategy', () => {
138
144
  },
139
145
  },
140
146
  testLogger,
147
+ {},
141
148
  ).getRebalancingRoutes({
142
149
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
143
150
  [chain2]: ethers.utils.parseEther('200').toBigInt(),
@@ -162,6 +169,7 @@ describe('WeightedStrategy', () => {
162
169
  },
163
170
  },
164
171
  testLogger,
172
+ {},
165
173
  ).getRebalancingRoutes({
166
174
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
167
175
  [chain3]: ethers.utils.parseEther('300').toBigInt(),
@@ -185,6 +193,7 @@ describe('WeightedStrategy', () => {
185
193
  },
186
194
  },
187
195
  testLogger,
196
+ {},
188
197
  ).getRebalancingRoutes({
189
198
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
190
199
  [chain2]: ethers.utils.parseEther('-200').toBigInt(),
@@ -207,6 +216,7 @@ describe('WeightedStrategy', () => {
207
216
  },
208
217
  },
209
218
  testLogger,
219
+ {},
210
220
  );
211
221
 
212
222
  const rawBalances = {
@@ -220,21 +230,20 @@ describe('WeightedStrategy', () => {
220
230
  });
221
231
 
222
232
  it('should return a single route when a chain is unbalanced', () => {
223
- const strategy = new WeightedStrategy(
224
- {
225
- [chain1]: {
226
- weighted: { weight: 100n, tolerance: 0n },
227
- bridge: ethers.constants.AddressZero,
228
- bridgeLockTime: 1,
229
- },
230
- [chain2]: {
231
- weighted: { weight: 100n, tolerance: 0n },
232
- bridge: ethers.constants.AddressZero,
233
- bridgeLockTime: 1,
234
- },
233
+ const config = {
234
+ [chain1]: {
235
+ weighted: { weight: 100n, tolerance: 0n },
236
+ bridge: ethers.constants.AddressZero,
237
+ bridgeLockTime: 1,
235
238
  },
236
- testLogger,
237
- );
239
+ [chain2]: {
240
+ weighted: { weight: 100n, tolerance: 0n },
241
+ bridge: ethers.constants.AddressZero,
242
+ bridgeLockTime: 1,
243
+ },
244
+ };
245
+ const bridgeConfigs = extractBridgeConfigs(config);
246
+ const strategy = new WeightedStrategy(config, testLogger, bridgeConfigs);
238
247
 
239
248
  const rawBalances = {
240
249
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
@@ -248,6 +257,7 @@ describe('WeightedStrategy', () => {
248
257
  origin: chain2,
249
258
  destination: chain1,
250
259
  amount: ethers.utils.parseEther('50').toBigInt(),
260
+ bridge: ethers.constants.AddressZero,
251
261
  },
252
262
  ]);
253
263
  });
@@ -267,6 +277,7 @@ describe('WeightedStrategy', () => {
267
277
  },
268
278
  },
269
279
  testLogger,
280
+ {},
270
281
  );
271
282
 
272
283
  const rawBalances = {
@@ -280,26 +291,25 @@ describe('WeightedStrategy', () => {
280
291
  });
281
292
 
282
293
  it('should return a single route when two chains are unbalanced and can be solved with a single transfer', () => {
283
- const strategy = new WeightedStrategy(
284
- {
285
- [chain1]: {
286
- weighted: { weight: 100n, tolerance: 0n },
287
- bridge: ethers.constants.AddressZero,
288
- bridgeLockTime: 1,
289
- },
290
- [chain2]: {
291
- weighted: { weight: 100n, tolerance: 0n },
292
- bridge: ethers.constants.AddressZero,
293
- bridgeLockTime: 1,
294
- },
295
- [chain3]: {
296
- weighted: { weight: 100n, tolerance: 0n },
297
- bridge: ethers.constants.AddressZero,
298
- bridgeLockTime: 1,
299
- },
294
+ const config = {
295
+ [chain1]: {
296
+ weighted: { weight: 100n, tolerance: 0n },
297
+ bridge: ethers.constants.AddressZero,
298
+ bridgeLockTime: 1,
300
299
  },
301
- testLogger,
302
- );
300
+ [chain2]: {
301
+ weighted: { weight: 100n, tolerance: 0n },
302
+ bridge: ethers.constants.AddressZero,
303
+ bridgeLockTime: 1,
304
+ },
305
+ [chain3]: {
306
+ weighted: { weight: 100n, tolerance: 0n },
307
+ bridge: ethers.constants.AddressZero,
308
+ bridgeLockTime: 1,
309
+ },
310
+ };
311
+ const bridgeConfigs = extractBridgeConfigs(config);
312
+ const strategy = new WeightedStrategy(config, testLogger, bridgeConfigs);
303
313
 
304
314
  const rawBalances = {
305
315
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
@@ -314,30 +324,30 @@ describe('WeightedStrategy', () => {
314
324
  origin: chain3,
315
325
  destination: chain1,
316
326
  amount: ethers.utils.parseEther('100').toBigInt(),
327
+ bridge: ethers.constants.AddressZero,
317
328
  },
318
329
  ]);
319
330
  });
320
331
  it('should return two routes when two chains are unbalanced and cannot be solved with a single transfer', () => {
321
- const strategy = new WeightedStrategy(
322
- {
323
- [chain1]: {
324
- weighted: { weight: 100n, tolerance: 0n },
325
- bridge: ethers.constants.AddressZero,
326
- bridgeLockTime: 1,
327
- },
328
- [chain2]: {
329
- weighted: { weight: 100n, tolerance: 0n },
330
- bridge: ethers.constants.AddressZero,
331
- bridgeLockTime: 1,
332
- },
333
- [chain3]: {
334
- weighted: { weight: 100n, tolerance: 0n },
335
- bridge: ethers.constants.AddressZero,
336
- bridgeLockTime: 1,
337
- },
332
+ const config = {
333
+ [chain1]: {
334
+ weighted: { weight: 100n, tolerance: 0n },
335
+ bridge: ethers.constants.AddressZero,
336
+ bridgeLockTime: 1,
338
337
  },
339
- testLogger,
340
- );
338
+ [chain2]: {
339
+ weighted: { weight: 100n, tolerance: 0n },
340
+ bridge: ethers.constants.AddressZero,
341
+ bridgeLockTime: 1,
342
+ },
343
+ [chain3]: {
344
+ weighted: { weight: 100n, tolerance: 0n },
345
+ bridge: ethers.constants.AddressZero,
346
+ bridgeLockTime: 1,
347
+ },
348
+ };
349
+ const bridgeConfigs = extractBridgeConfigs(config);
350
+ const strategy = new WeightedStrategy(config, testLogger, bridgeConfigs);
341
351
 
342
352
  const rawBalances = {
343
353
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
@@ -352,36 +362,37 @@ describe('WeightedStrategy', () => {
352
362
  origin: chain3,
353
363
  destination: chain1,
354
364
  amount: 133333333333333333333n,
365
+ bridge: ethers.constants.AddressZero,
355
366
  },
356
367
  {
357
368
  origin: chain3,
358
369
  destination: chain2,
359
370
  amount: 133333333333333333333n,
371
+ bridge: ethers.constants.AddressZero,
360
372
  },
361
373
  ]);
362
374
  });
363
375
 
364
376
  it('should return routes to balance different weighted chains', () => {
365
- const strategy = new WeightedStrategy(
366
- {
367
- [chain1]: {
368
- weighted: { weight: 50n, tolerance: 0n },
369
- bridge: ethers.constants.AddressZero,
370
- bridgeLockTime: 1,
371
- },
372
- [chain2]: {
373
- weighted: { weight: 25n, tolerance: 0n },
374
- bridge: ethers.constants.AddressZero,
375
- bridgeLockTime: 1,
376
- },
377
- [chain3]: {
378
- weighted: { weight: 25n, tolerance: 0n },
379
- bridge: ethers.constants.AddressZero,
380
- bridgeLockTime: 1,
381
- },
377
+ const config = {
378
+ [chain1]: {
379
+ weighted: { weight: 50n, tolerance: 0n },
380
+ bridge: ethers.constants.AddressZero,
381
+ bridgeLockTime: 1,
382
382
  },
383
- testLogger,
384
- );
383
+ [chain2]: {
384
+ weighted: { weight: 25n, tolerance: 0n },
385
+ bridge: ethers.constants.AddressZero,
386
+ bridgeLockTime: 1,
387
+ },
388
+ [chain3]: {
389
+ weighted: { weight: 25n, tolerance: 0n },
390
+ bridge: ethers.constants.AddressZero,
391
+ bridgeLockTime: 1,
392
+ },
393
+ };
394
+ const bridgeConfigs = extractBridgeConfigs(config);
395
+ const strategy = new WeightedStrategy(config, testLogger, bridgeConfigs);
385
396
 
386
397
  const rawBalances = {
387
398
  [chain1]: ethers.utils.parseEther('100').toBigInt(),
@@ -396,13 +407,117 @@ describe('WeightedStrategy', () => {
396
407
  origin: chain2,
397
408
  destination: chain1,
398
409
  amount: ethers.utils.parseEther('25').toBigInt(),
410
+ bridge: ethers.constants.AddressZero,
399
411
  },
400
412
  {
401
413
  origin: chain3,
402
414
  destination: chain1,
403
415
  amount: ethers.utils.parseEther('25').toBigInt(),
416
+ bridge: ethers.constants.AddressZero,
404
417
  },
405
418
  ]);
406
419
  });
407
420
  });
421
+
422
+ describe('bridgeMinAcceptedAmount filtering', () => {
423
+ function createMockToken(chainName: string, decimals = 18): Token {
424
+ return {
425
+ chainName,
426
+ decimals,
427
+ addressOrDenom: ethers.constants.AddressZero,
428
+ } as unknown as Token;
429
+ }
430
+
431
+ it('should filter out routes below bridgeMinAcceptedAmount', () => {
432
+ const chain1 = 'chain1';
433
+ const chain2 = 'chain2';
434
+
435
+ const tokensByChainName: ChainMap<Token> = {
436
+ [chain1]: createMockToken(chain1),
437
+ [chain2]: createMockToken(chain2),
438
+ };
439
+
440
+ const config = {
441
+ [chain1]: {
442
+ weighted: { weight: 50n, tolerance: 0n },
443
+ bridge: ethers.constants.AddressZero,
444
+ bridgeLockTime: 1,
445
+ bridgeMinAcceptedAmount: '100', // 100 tokens minimum
446
+ },
447
+ [chain2]: {
448
+ weighted: { weight: 50n, tolerance: 0n },
449
+ bridge: ethers.constants.AddressZero,
450
+ bridgeLockTime: 1,
451
+ bridgeMinAcceptedAmount: '100',
452
+ },
453
+ };
454
+ const bridgeConfigs = extractBridgeConfigs(config);
455
+ const strategy = new WeightedStrategy(
456
+ config,
457
+ testLogger,
458
+ bridgeConfigs,
459
+ undefined,
460
+ tokensByChainName,
461
+ );
462
+
463
+ // chain1 has 150, chain2 has 50 (total 200, each should have 100)
464
+ // Would generate route: chain1 -> chain2, amount = 50
465
+ // But 50 < bridgeMinAcceptedAmount (100), so route should be filtered
466
+ const rawBalances: RawBalances = {
467
+ [chain1]: ethers.utils.parseEther('150').toBigInt(),
468
+ [chain2]: ethers.utils.parseEther('50').toBigInt(),
469
+ };
470
+
471
+ const routes = strategy.getRebalancingRoutes(rawBalances);
472
+
473
+ expect(routes).to.have.lengthOf(0);
474
+ });
475
+
476
+ it('should keep routes at or above bridgeMinAcceptedAmount', () => {
477
+ const chain1 = 'chain1';
478
+ const chain2 = 'chain2';
479
+
480
+ const tokensByChainName: ChainMap<Token> = {
481
+ [chain1]: createMockToken(chain1),
482
+ [chain2]: createMockToken(chain2),
483
+ };
484
+
485
+ const config = {
486
+ [chain1]: {
487
+ weighted: { weight: 50n, tolerance: 0n },
488
+ bridge: ethers.constants.AddressZero,
489
+ bridgeLockTime: 1,
490
+ bridgeMinAcceptedAmount: '50', // 50 tokens minimum
491
+ },
492
+ [chain2]: {
493
+ weighted: { weight: 50n, tolerance: 0n },
494
+ bridge: ethers.constants.AddressZero,
495
+ bridgeLockTime: 1,
496
+ bridgeMinAcceptedAmount: '50',
497
+ },
498
+ };
499
+ const bridgeConfigs = extractBridgeConfigs(config);
500
+ const strategy = new WeightedStrategy(
501
+ config,
502
+ testLogger,
503
+ bridgeConfigs,
504
+ undefined,
505
+ tokensByChainName,
506
+ );
507
+
508
+ // chain1 has 200, chain2 has 100 (total 300, each should have 150)
509
+ // Route: chain1 -> chain2, amount = 50 (equals minAcceptedAmount)
510
+ const rawBalances: RawBalances = {
511
+ [chain1]: ethers.utils.parseEther('200').toBigInt(),
512
+ [chain2]: ethers.utils.parseEther('100').toBigInt(),
513
+ };
514
+
515
+ const routes = strategy.getRebalancingRoutes(rawBalances);
516
+
517
+ expect(routes).to.have.lengthOf(1);
518
+ expect(routes[0].amount).to.equal(
519
+ ethers.utils.parseEther('50').toBigInt(),
520
+ );
521
+ });
522
+ });
408
523
  });
@@ -1,8 +1,18 @@
1
1
  import { type Logger } from 'pino';
2
2
 
3
- import type { WeightedStrategyConfig } from '../config/types.js';
4
- import type { RawBalances } from '../interfaces/IStrategy.js';
3
+ import type { ChainMap, Token } from '@hyperlane-xyz/sdk';
4
+
5
+ import {
6
+ RebalancerStrategyOptions,
7
+ type WeightedStrategyConfig,
8
+ } from '../config/types.js';
9
+ import type {
10
+ RawBalances,
11
+ Route,
12
+ StrategyRoute,
13
+ } from '../interfaces/IStrategy.js';
5
14
  import { type Metrics } from '../metrics/Metrics.js';
15
+ import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
6
16
 
7
17
  import { BaseStrategy, type Delta } from './BaseStrategy.js';
8
18
 
@@ -11,6 +21,7 @@ import { BaseStrategy, type Delta } from './BaseStrategy.js';
11
21
  * It distributes funds across chains based on their weights
12
22
  */
13
23
  export class WeightedStrategy extends BaseStrategy {
24
+ readonly name = RebalancerStrategyOptions.Weighted;
14
25
  private readonly config: WeightedStrategyConfig;
15
26
  private readonly totalWeight: bigint;
16
27
  protected readonly logger: Logger;
@@ -18,11 +29,13 @@ export class WeightedStrategy extends BaseStrategy {
18
29
  constructor(
19
30
  config: WeightedStrategyConfig,
20
31
  logger: Logger,
32
+ bridgeConfigs: ChainMap<BridgeConfigWithOverride>,
21
33
  metrics?: Metrics,
34
+ tokensByChainName?: ChainMap<Token>,
22
35
  ) {
23
36
  const chains = Object.keys(config);
24
37
  const log = logger.child({ class: WeightedStrategy.name });
25
- super(chains, log, metrics);
38
+ super(chains, log, bridgeConfigs, metrics, tokensByChainName);
26
39
  this.logger = log;
27
40
 
28
41
  let totalWeight = 0n;
@@ -54,14 +67,35 @@ export class WeightedStrategy extends BaseStrategy {
54
67
 
55
68
  /**
56
69
  * Gets balances categorized by surplus and deficit based on weights
70
+ *
71
+ * Simulates both types of rebalances before calculating surpluses/deficits:
72
+ * - pendingRebalances: in-flight intents (origin tx confirmed, add to destination only)
73
+ * - proposedRebalances: routes from earlier strategies (subtract from origin AND add to destination)
74
+ *
75
+ * This prevents over-rebalancing when multiple strategies run in sequence.
57
76
  */
58
- protected getCategorizedBalances(rawBalances: RawBalances): {
77
+ protected getCategorizedBalances(
78
+ rawBalances: RawBalances,
79
+ pendingRebalances?: Route[],
80
+ proposedRebalances?: StrategyRoute[],
81
+ ): {
59
82
  surpluses: Delta[];
60
83
  deficits: Delta[];
61
84
  } {
62
- // Get the total balance from all chains
85
+ // Step 1: Simulate pending rebalances (in-flight, origin already deducted on-chain)
86
+ let simulatedBalances = this.simulatePendingRebalances(
87
+ rawBalances,
88
+ pendingRebalances ?? [],
89
+ );
90
+
91
+ // Step 2: Simulate proposed rebalances (from earlier strategies, not yet executed)
92
+ simulatedBalances = this.simulateProposedRebalances(
93
+ simulatedBalances,
94
+ proposedRebalances ?? [],
95
+ );
96
+ // Get the total balance from all chains (using simulated balances)
63
97
  const total = this.chains.reduce(
64
- (sum, chain) => sum + rawBalances[chain],
98
+ (sum, chain) => sum + simulatedBalances[chain],
65
99
  0n,
66
100
  );
67
101
 
@@ -70,7 +104,7 @@ export class WeightedStrategy extends BaseStrategy {
70
104
  const { weight, tolerance } = this.config[chain].weighted;
71
105
  const target = (total * weight) / this.totalWeight;
72
106
  const toleranceAmount = (target * tolerance) / 100n;
73
- const balance = rawBalances[chain];
107
+ const balance = simulatedBalances[chain];
74
108
 
75
109
  // Apply the tolerance to deficits to prevent small imbalances
76
110
  if (balance < target - toleranceAmount) {
@@ -1,4 +1,6 @@
1
1
  export { BaseStrategy } from './BaseStrategy.js';
2
+ export { CollateralDeficitStrategy } from './CollateralDeficitStrategy.js';
3
+ export { CompositeStrategy } from './CompositeStrategy.js';
2
4
  export { MinAmountStrategy } from './MinAmountStrategy.js';
3
5
  export { StrategyFactory } from './StrategyFactory.js';
4
6
  export { WeightedStrategy } from './WeightedStrategy.js';