@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
@@ -3,6 +3,7 @@ import { ethers } from 'ethers';
3
3
  import { rmSync } from 'fs';
4
4
  import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
+ import type { z } from 'zod';
6
7
 
7
8
  import { writeYamlOrJson } from '@hyperlane-xyz/utils/fs';
8
9
 
@@ -11,37 +12,49 @@ import {
11
12
  type RebalancerConfigFileInput,
12
13
  RebalancerMinAmountType,
13
14
  RebalancerStrategyOptions,
15
+ type StrategyConfig,
16
+ getAllBridges,
14
17
  } from './types.js';
15
18
 
16
19
  const TEST_CONFIG_PATH = join(tmpdir(), 'rebalancer-config-test.yaml');
17
20
 
21
+ // Helper to get strategy as array (for test type safety)
22
+ // Schema accepts both single object and array, but tests use array format
23
+ function getStrategyArray(
24
+ data: RebalancerConfigFileInput,
25
+ ): z.input<typeof import('./types.js').StrategyConfigSchema>[] {
26
+ return Array.isArray(data.strategy) ? data.strategy : [data.strategy];
27
+ }
28
+
18
29
  describe('RebalancerConfig', () => {
19
30
  let data: RebalancerConfigFileInput;
20
31
 
21
32
  beforeEach(() => {
22
33
  data = {
23
34
  warpRouteId: 'warpRouteId',
24
- strategy: {
25
- rebalanceStrategy: RebalancerStrategyOptions.Weighted,
26
- chains: {
27
- chain1: {
28
- weighted: {
29
- weight: 100,
30
- tolerance: 0,
35
+ strategy: [
36
+ {
37
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
38
+ chains: {
39
+ chain1: {
40
+ weighted: {
41
+ weight: 100,
42
+ tolerance: 0,
43
+ },
44
+ bridge: ethers.constants.AddressZero,
45
+ bridgeLockTime: 1,
31
46
  },
32
- bridge: ethers.constants.AddressZero,
33
- bridgeLockTime: 1,
34
- },
35
- chain2: {
36
- weighted: {
37
- weight: 100,
38
- tolerance: 0,
47
+ chain2: {
48
+ weighted: {
49
+ weight: 100,
50
+ tolerance: 0,
51
+ },
52
+ bridge: ethers.constants.AddressZero,
53
+ bridgeLockTime: 1,
39
54
  },
40
- bridge: ethers.constants.AddressZero,
41
- bridgeLockTime: 1,
42
55
  },
43
56
  },
44
- },
57
+ ],
45
58
  };
46
59
 
47
60
  writeYamlOrJson(TEST_CONFIG_PATH, data);
@@ -62,32 +75,34 @@ describe('RebalancerConfig', () => {
62
75
  it('should load config from file', () => {
63
76
  expect(RebalancerConfig.load(TEST_CONFIG_PATH)).to.deep.equal({
64
77
  warpRouteId: 'warpRouteId',
65
- strategyConfig: {
66
- rebalanceStrategy: RebalancerStrategyOptions.Weighted,
67
- chains: {
68
- chain1: {
69
- weighted: {
70
- weight: 100n,
71
- tolerance: 0n,
78
+ strategyConfig: [
79
+ {
80
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
81
+ chains: {
82
+ chain1: {
83
+ weighted: {
84
+ weight: 100n,
85
+ tolerance: 0n,
86
+ },
87
+ bridge: ethers.constants.AddressZero,
88
+ bridgeLockTime: 1_000,
72
89
  },
73
- bridge: ethers.constants.AddressZero,
74
- bridgeLockTime: 1_000,
75
- },
76
- chain2: {
77
- weighted: {
78
- weight: 100n,
79
- tolerance: 0n,
90
+ chain2: {
91
+ weighted: {
92
+ weight: 100n,
93
+ tolerance: 0n,
94
+ },
95
+ bridge: ethers.constants.AddressZero,
96
+ bridgeLockTime: 1_000,
80
97
  },
81
- bridge: ethers.constants.AddressZero,
82
- bridgeLockTime: 1_000,
83
98
  },
84
99
  },
85
- },
100
+ ],
86
101
  });
87
102
  });
88
103
 
89
104
  it('should throw if chains are not configured', () => {
90
- data.strategy.chains = {};
105
+ getStrategyArray(data)[0].chains = {};
91
106
 
92
107
  writeYamlOrJson(TEST_CONFIG_PATH, data);
93
108
 
@@ -110,37 +125,39 @@ describe('RebalancerConfig', () => {
110
125
  it('should load relative params without modifications', () => {
111
126
  data = {
112
127
  warpRouteId: 'warpRouteId',
113
- strategy: {
114
- rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
115
- chains: {
116
- chain1: {
117
- minAmount: {
118
- min: '0.2',
119
- target: 0.3,
120
- type: RebalancerMinAmountType.Relative,
128
+ strategy: [
129
+ {
130
+ rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
131
+ chains: {
132
+ chain1: {
133
+ minAmount: {
134
+ min: '0.2',
135
+ target: 0.3,
136
+ type: RebalancerMinAmountType.Relative,
137
+ },
138
+ bridge: ethers.constants.AddressZero,
139
+ bridgeLockTime: 1,
121
140
  },
122
- bridge: ethers.constants.AddressZero,
123
- bridgeLockTime: 1,
124
- },
125
- chain2: {
126
- minAmount: {
127
- min: '0.2',
128
- target: 0.3,
129
- type: RebalancerMinAmountType.Relative,
141
+ chain2: {
142
+ minAmount: {
143
+ min: '0.2',
144
+ target: 0.3,
145
+ type: RebalancerMinAmountType.Relative,
146
+ },
147
+ bridge: ethers.constants.AddressZero,
148
+ bridgeLockTime: 1,
130
149
  },
131
- bridge: ethers.constants.AddressZero,
132
- bridgeLockTime: 1,
133
150
  },
134
151
  },
135
- },
152
+ ],
136
153
  };
137
154
 
138
155
  writeYamlOrJson(TEST_CONFIG_PATH, data);
139
156
 
140
157
  expect(
141
- RebalancerConfig.load(TEST_CONFIG_PATH).strategyConfig.chains.chain1,
158
+ RebalancerConfig.load(TEST_CONFIG_PATH).strategyConfig[0].chains.chain1,
142
159
  ).to.deep.equal({
143
- ...data.strategy.chains.chain1,
160
+ ...getStrategyArray(data)[0].chains.chain1,
144
161
  bridgeLockTime: 1_000,
145
162
  });
146
163
  });
@@ -148,37 +165,39 @@ describe('RebalancerConfig', () => {
148
165
  it('should load absolute params without modifications', () => {
149
166
  data = {
150
167
  warpRouteId: 'warpRouteId',
151
- strategy: {
152
- rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
153
- chains: {
154
- chain1: {
155
- minAmount: {
156
- min: '100000',
157
- target: 140000,
158
- type: RebalancerMinAmountType.Absolute,
168
+ strategy: [
169
+ {
170
+ rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
171
+ chains: {
172
+ chain1: {
173
+ minAmount: {
174
+ min: '100000',
175
+ target: 140000,
176
+ type: RebalancerMinAmountType.Absolute,
177
+ },
178
+ bridge: ethers.constants.AddressZero,
179
+ bridgeLockTime: 1,
159
180
  },
160
- bridge: ethers.constants.AddressZero,
161
- bridgeLockTime: 1,
162
- },
163
- chain2: {
164
- minAmount: {
165
- min: '100000',
166
- target: 140000,
167
- type: RebalancerMinAmountType.Absolute,
181
+ chain2: {
182
+ minAmount: {
183
+ min: '100000',
184
+ target: 140000,
185
+ type: RebalancerMinAmountType.Absolute,
186
+ },
187
+ bridge: ethers.constants.AddressZero,
188
+ bridgeLockTime: 1,
168
189
  },
169
- bridge: ethers.constants.AddressZero,
170
- bridgeLockTime: 1,
171
190
  },
172
191
  },
173
- },
192
+ ],
174
193
  };
175
194
 
176
195
  writeYamlOrJson(TEST_CONFIG_PATH, data);
177
196
 
178
197
  expect(
179
- RebalancerConfig.load(TEST_CONFIG_PATH).strategyConfig.chains.chain1,
198
+ RebalancerConfig.load(TEST_CONFIG_PATH).strategyConfig[0].chains.chain1,
180
199
  ).to.deep.equal({
181
- ...data.strategy.chains.chain1,
200
+ ...getStrategyArray(data)[0].chains.chain1,
182
201
  bridgeLockTime: 1_000,
183
202
  });
184
203
  });
@@ -187,51 +206,54 @@ describe('RebalancerConfig', () => {
187
206
  it('should parse a config with overrides', () => {
188
207
  data = {
189
208
  warpRouteId: 'warpRouteId',
190
- strategy: {
191
- rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
192
- chains: {
193
- chain1: {
194
- minAmount: {
195
- min: 1000,
196
- target: 1100,
197
- type: RebalancerMinAmountType.Absolute,
198
- },
199
- bridge: ethers.constants.AddressZero,
200
- bridgeLockTime: 1,
201
- override: {
202
- chain2: {
203
- bridge: '0x1234567890123456789012345678901234567890',
209
+ strategy: [
210
+ {
211
+ rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
212
+ chains: {
213
+ chain1: {
214
+ minAmount: {
215
+ min: 1000,
216
+ target: 1100,
217
+ type: RebalancerMinAmountType.Absolute,
218
+ },
219
+ bridge: ethers.constants.AddressZero,
220
+ bridgeLockTime: 1,
221
+ override: {
222
+ chain2: {
223
+ bridge: '0x1234567890123456789012345678901234567890',
224
+ },
204
225
  },
205
226
  },
206
- },
207
- chain2: {
208
- minAmount: {
209
- min: 2000,
210
- target: 2200,
211
- type: RebalancerMinAmountType.Absolute,
227
+ chain2: {
228
+ minAmount: {
229
+ min: 2000,
230
+ target: 2200,
231
+ type: RebalancerMinAmountType.Absolute,
232
+ },
233
+ bridge: ethers.constants.AddressZero,
234
+ bridgeLockTime: 1,
212
235
  },
213
- bridge: ethers.constants.AddressZero,
214
- bridgeLockTime: 1,
215
- },
216
- chain3: {
217
- minAmount: {
218
- min: 3000,
219
- target: 3300,
220
- type: RebalancerMinAmountType.Absolute,
236
+ chain3: {
237
+ minAmount: {
238
+ min: 3000,
239
+ target: 3300,
240
+ type: RebalancerMinAmountType.Absolute,
241
+ },
242
+ bridge: ethers.constants.AddressZero,
243
+ bridgeLockTime: 1,
221
244
  },
222
- bridge: ethers.constants.AddressZero,
223
- bridgeLockTime: 1,
224
245
  },
225
246
  },
226
- },
247
+ ],
227
248
  };
228
249
 
229
250
  writeYamlOrJson(TEST_CONFIG_PATH, data);
230
251
 
231
252
  const config = RebalancerConfig.load(TEST_CONFIG_PATH);
232
- expect(config.strategyConfig.chains.chain1).to.have.property('override');
253
+ const chainConfig = config.strategyConfig[0].chains.chain1;
254
+ expect(chainConfig).to.have.property('override');
233
255
 
234
- const override = config.strategyConfig.chains.chain1.override;
256
+ const override = chainConfig.override;
235
257
  expect(override).to.not.be.undefined;
236
258
  expect(override).to.have.property('chain2');
237
259
 
@@ -245,37 +267,39 @@ describe('RebalancerConfig', () => {
245
267
  it('should throw when an override references a non-existent chain', () => {
246
268
  data = {
247
269
  warpRouteId: 'warpRouteId',
248
- strategy: {
249
- rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
250
- chains: {
251
- chain1: {
252
- minAmount: {
253
- min: 1000,
254
- target: 1100,
255
- type: RebalancerMinAmountType.Absolute,
256
- },
257
- bridge: ethers.constants.AddressZero,
258
- bridgeLockTime: 1,
259
- override: {
260
- chain2: {
261
- bridge: '0x1234567890123456789012345678901234567890',
270
+ strategy: [
271
+ {
272
+ rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
273
+ chains: {
274
+ chain1: {
275
+ minAmount: {
276
+ min: 1000,
277
+ target: 1100,
278
+ type: RebalancerMinAmountType.Absolute,
262
279
  },
263
- chain3: {
264
- bridgeMinAcceptedAmount: 1000,
280
+ bridge: ethers.constants.AddressZero,
281
+ bridgeLockTime: 1,
282
+ override: {
283
+ chain2: {
284
+ bridge: '0x1234567890123456789012345678901234567890',
285
+ },
286
+ chain3: {
287
+ bridgeMinAcceptedAmount: 1000,
288
+ },
265
289
  },
266
290
  },
267
- },
268
- chain2: {
269
- minAmount: {
270
- min: 2000,
271
- target: 2200,
272
- type: RebalancerMinAmountType.Absolute,
291
+ chain2: {
292
+ minAmount: {
293
+ min: 2000,
294
+ target: 2200,
295
+ type: RebalancerMinAmountType.Absolute,
296
+ },
297
+ bridge: ethers.constants.AddressZero,
298
+ bridgeLockTime: 1,
273
299
  },
274
- bridge: ethers.constants.AddressZero,
275
- bridgeLockTime: 1,
276
300
  },
277
301
  },
278
- },
302
+ ],
279
303
  };
280
304
 
281
305
  writeYamlOrJson(TEST_CONFIG_PATH, data);
@@ -288,34 +312,36 @@ describe('RebalancerConfig', () => {
288
312
  it('should throw when an override references itself', () => {
289
313
  data = {
290
314
  warpRouteId: 'warpRouteId',
291
- strategy: {
292
- rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
293
- chains: {
294
- chain1: {
295
- minAmount: {
296
- min: 1000,
297
- target: 1100,
298
- type: RebalancerMinAmountType.Absolute,
299
- },
300
- bridge: ethers.constants.AddressZero,
301
- bridgeLockTime: 1,
302
- override: {
303
- chain1: {
304
- bridgeMinAcceptedAmount: 1000,
315
+ strategy: [
316
+ {
317
+ rebalanceStrategy: RebalancerStrategyOptions.MinAmount,
318
+ chains: {
319
+ chain1: {
320
+ minAmount: {
321
+ min: 1000,
322
+ target: 1100,
323
+ type: RebalancerMinAmountType.Absolute,
324
+ },
325
+ bridge: ethers.constants.AddressZero,
326
+ bridgeLockTime: 1,
327
+ override: {
328
+ chain1: {
329
+ bridgeMinAcceptedAmount: 1000,
330
+ },
305
331
  },
306
332
  },
307
- },
308
- chain2: {
309
- minAmount: {
310
- min: 2000,
311
- target: 2200,
312
- type: RebalancerMinAmountType.Absolute,
333
+ chain2: {
334
+ minAmount: {
335
+ min: 2000,
336
+ target: 2200,
337
+ type: RebalancerMinAmountType.Absolute,
338
+ },
339
+ bridge: ethers.constants.AddressZero,
340
+ bridgeLockTime: 1,
313
341
  },
314
- bridge: ethers.constants.AddressZero,
315
- bridgeLockTime: 1,
316
342
  },
317
343
  },
318
- },
344
+ ],
319
345
  };
320
346
 
321
347
  writeYamlOrJson(TEST_CONFIG_PATH, data);
@@ -326,7 +352,7 @@ describe('RebalancerConfig', () => {
326
352
  });
327
353
 
328
354
  it('should allow multiple chain overrides', () => {
329
- data.strategy.chains.chain1 = {
355
+ getStrategyArray(data)[0].chains.chain1 = {
330
356
  bridge: ethers.constants.AddressZero,
331
357
  bridgeMinAcceptedAmount: 3000,
332
358
  bridgeLockTime: 1,
@@ -344,7 +370,7 @@ describe('RebalancerConfig', () => {
344
370
  },
345
371
  };
346
372
 
347
- data.strategy.chains.chain2 = {
373
+ getStrategyArray(data)[0].chains.chain2 = {
348
374
  bridge: ethers.constants.AddressZero,
349
375
  bridgeMinAcceptedAmount: 5000,
350
376
  bridgeLockTime: 1,
@@ -354,7 +380,7 @@ describe('RebalancerConfig', () => {
354
380
  },
355
381
  };
356
382
 
357
- data.strategy.chains.chain3 = {
383
+ getStrategyArray(data)[0].chains.chain3 = {
358
384
  bridge: ethers.constants.AddressZero,
359
385
  bridgeMinAcceptedAmount: 6000,
360
386
  bridgeLockTime: 1,
@@ -367,8 +393,8 @@ describe('RebalancerConfig', () => {
367
393
  writeYamlOrJson(TEST_CONFIG_PATH, data);
368
394
 
369
395
  const config = RebalancerConfig.load(TEST_CONFIG_PATH);
370
-
371
- const chain1Overrides = config.strategyConfig.chains.chain1.override;
396
+ const chainConfig = config.strategyConfig[0].chains.chain1;
397
+ const chain1Overrides = chainConfig.override;
372
398
  expect(chain1Overrides).to.not.be.undefined;
373
399
  expect(chain1Overrides).to.have.property('chain2');
374
400
  expect(chain1Overrides).to.have.property('chain3');
@@ -384,4 +410,274 @@ describe('RebalancerConfig', () => {
384
410
  );
385
411
  });
386
412
  });
413
+
414
+ describe('composite strategy validation', () => {
415
+ it('should throw if CollateralDeficitStrategy is not first in composite', () => {
416
+ data = {
417
+ warpRouteId: 'warpRouteId',
418
+ strategy: [
419
+ {
420
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
421
+ chains: {
422
+ chain1: {
423
+ weighted: { weight: 100, tolerance: 0 },
424
+ bridge: ethers.constants.AddressZero,
425
+ bridgeLockTime: 1,
426
+ },
427
+ chain2: {
428
+ weighted: { weight: 100, tolerance: 0 },
429
+ bridge: ethers.constants.AddressZero,
430
+ bridgeLockTime: 1,
431
+ },
432
+ },
433
+ },
434
+ {
435
+ rebalanceStrategy: RebalancerStrategyOptions.CollateralDeficit,
436
+ chains: {
437
+ chain1: {
438
+ buffer: 1000,
439
+ bridge: ethers.constants.AddressZero,
440
+ bridgeLockTime: 1,
441
+ },
442
+ chain2: {
443
+ buffer: 1000,
444
+ bridge: ethers.constants.AddressZero,
445
+ bridgeLockTime: 1,
446
+ },
447
+ },
448
+ },
449
+ ],
450
+ };
451
+
452
+ writeYamlOrJson(TEST_CONFIG_PATH, data);
453
+
454
+ expect(() => RebalancerConfig.load(TEST_CONFIG_PATH)).to.throw(
455
+ 'CollateralDeficitStrategy must be first when used in composite strategy',
456
+ );
457
+ });
458
+
459
+ it('should allow CollateralDeficitStrategy first in composite', () => {
460
+ data = {
461
+ warpRouteId: 'warpRouteId',
462
+ strategy: [
463
+ {
464
+ rebalanceStrategy: RebalancerStrategyOptions.CollateralDeficit,
465
+ chains: {
466
+ chain1: {
467
+ buffer: 1000,
468
+ bridge: ethers.constants.AddressZero,
469
+ bridgeLockTime: 1,
470
+ },
471
+ chain2: {
472
+ buffer: 1000,
473
+ bridge: ethers.constants.AddressZero,
474
+ bridgeLockTime: 1,
475
+ },
476
+ },
477
+ },
478
+ {
479
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
480
+ chains: {
481
+ chain1: {
482
+ weighted: { weight: 100, tolerance: 0 },
483
+ bridge: ethers.constants.AddressZero,
484
+ bridgeLockTime: 1,
485
+ },
486
+ chain2: {
487
+ weighted: { weight: 100, tolerance: 0 },
488
+ bridge: ethers.constants.AddressZero,
489
+ bridgeLockTime: 1,
490
+ },
491
+ },
492
+ },
493
+ ],
494
+ };
495
+
496
+ writeYamlOrJson(TEST_CONFIG_PATH, data);
497
+
498
+ expect(() => RebalancerConfig.load(TEST_CONFIG_PATH)).to.not.throw();
499
+ });
500
+ });
501
+ });
502
+
503
+ describe('getAllBridges', () => {
504
+ const BRIDGE_A = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
505
+ const BRIDGE_B = '0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
506
+ const BRIDGE_C = '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC';
507
+
508
+ it('should return empty array for empty strategies', () => {
509
+ const result = getAllBridges([]);
510
+ expect(result).to.deep.equal([]);
511
+ });
512
+
513
+ it('should return bridge from single strategy', () => {
514
+ const strategies: StrategyConfig[] = [
515
+ {
516
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
517
+ chains: {
518
+ chain1: {
519
+ weighted: { weight: 100n, tolerance: 0n },
520
+ bridge: BRIDGE_A,
521
+ bridgeLockTime: 1000,
522
+ },
523
+ chain2: {
524
+ weighted: { weight: 100n, tolerance: 0n },
525
+ bridge: BRIDGE_A,
526
+ bridgeLockTime: 1000,
527
+ },
528
+ },
529
+ },
530
+ ];
531
+
532
+ const result = getAllBridges(strategies);
533
+ expect(result).to.deep.equal([BRIDGE_A]);
534
+ });
535
+
536
+ it('should return all bridges from multiple strategies', () => {
537
+ const strategies: StrategyConfig[] = [
538
+ {
539
+ rebalanceStrategy: RebalancerStrategyOptions.CollateralDeficit,
540
+ chains: {
541
+ chain1: {
542
+ buffer: 1000,
543
+ bridge: BRIDGE_A,
544
+ bridgeLockTime: 1000,
545
+ },
546
+ chain2: {
547
+ buffer: 1000,
548
+ bridge: BRIDGE_A,
549
+ bridgeLockTime: 1000,
550
+ },
551
+ },
552
+ },
553
+ {
554
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
555
+ chains: {
556
+ chain1: {
557
+ weighted: { weight: 100n, tolerance: 0n },
558
+ bridge: BRIDGE_B,
559
+ bridgeLockTime: 1000,
560
+ },
561
+ chain2: {
562
+ weighted: { weight: 100n, tolerance: 0n },
563
+ bridge: BRIDGE_B,
564
+ bridgeLockTime: 1000,
565
+ },
566
+ },
567
+ },
568
+ ];
569
+
570
+ const result = getAllBridges(strategies);
571
+ expect(result).to.have.members([BRIDGE_A, BRIDGE_B]);
572
+ expect(result).to.have.lengthOf(2);
573
+ });
574
+
575
+ it('should include bridges from per-destination overrides', () => {
576
+ const strategies: StrategyConfig[] = [
577
+ {
578
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
579
+ chains: {
580
+ chain1: {
581
+ weighted: { weight: 100n, tolerance: 0n },
582
+ bridge: BRIDGE_A,
583
+ bridgeLockTime: 1000,
584
+ override: {
585
+ chain2: {
586
+ bridge: BRIDGE_B,
587
+ },
588
+ chain3: {
589
+ bridge: BRIDGE_C,
590
+ },
591
+ },
592
+ },
593
+ chain2: {
594
+ weighted: { weight: 100n, tolerance: 0n },
595
+ bridge: BRIDGE_A,
596
+ bridgeLockTime: 1000,
597
+ },
598
+ chain3: {
599
+ weighted: { weight: 100n, tolerance: 0n },
600
+ bridge: BRIDGE_A,
601
+ bridgeLockTime: 1000,
602
+ },
603
+ },
604
+ },
605
+ ];
606
+
607
+ const result = getAllBridges(strategies);
608
+ expect(result).to.have.members([BRIDGE_A, BRIDGE_B, BRIDGE_C]);
609
+ expect(result).to.have.lengthOf(3);
610
+ });
611
+
612
+ it('should deduplicate bridges across strategies and overrides', () => {
613
+ const strategies: StrategyConfig[] = [
614
+ {
615
+ rebalanceStrategy: RebalancerStrategyOptions.CollateralDeficit,
616
+ chains: {
617
+ chain1: {
618
+ buffer: 1000,
619
+ bridge: BRIDGE_A,
620
+ bridgeLockTime: 1000,
621
+ },
622
+ chain2: {
623
+ buffer: 1000,
624
+ bridge: BRIDGE_B,
625
+ bridgeLockTime: 1000,
626
+ },
627
+ },
628
+ },
629
+ {
630
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
631
+ chains: {
632
+ chain1: {
633
+ weighted: { weight: 100n, tolerance: 0n },
634
+ bridge: BRIDGE_A, // Same as first strategy
635
+ bridgeLockTime: 1000,
636
+ override: {
637
+ chain2: {
638
+ bridge: BRIDGE_B, // Same as chain2 default
639
+ },
640
+ },
641
+ },
642
+ chain2: {
643
+ weighted: { weight: 100n, tolerance: 0n },
644
+ bridge: BRIDGE_B,
645
+ bridgeLockTime: 1000,
646
+ },
647
+ },
648
+ },
649
+ ];
650
+
651
+ const result = getAllBridges(strategies);
652
+ expect(result).to.have.members([BRIDGE_A, BRIDGE_B]);
653
+ expect(result).to.have.lengthOf(2);
654
+ });
655
+
656
+ it('should handle overrides without bridge property', () => {
657
+ const strategies: StrategyConfig[] = [
658
+ {
659
+ rebalanceStrategy: RebalancerStrategyOptions.Weighted,
660
+ chains: {
661
+ chain1: {
662
+ weighted: { weight: 100n, tolerance: 0n },
663
+ bridge: BRIDGE_A,
664
+ bridgeLockTime: 1000,
665
+ override: {
666
+ chain2: {
667
+ bridgeMinAcceptedAmount: 5000, // Override without bridge
668
+ },
669
+ },
670
+ },
671
+ chain2: {
672
+ weighted: { weight: 100n, tolerance: 0n },
673
+ bridge: BRIDGE_A,
674
+ bridgeLockTime: 1000,
675
+ },
676
+ },
677
+ },
678
+ ];
679
+
680
+ const result = getAllBridges(strategies);
681
+ expect(result).to.deep.equal([BRIDGE_A]);
682
+ });
387
683
  });