@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
@@ -0,0 +1,551 @@
1
+ import { expect } from 'chai';
2
+ import { pino } from 'pino';
3
+
4
+ import {
5
+ type ChainMap,
6
+ type ChainName,
7
+ Token,
8
+ TokenStandard,
9
+ } from '@hyperlane-xyz/sdk';
10
+ import type { Address } from '@hyperlane-xyz/utils';
11
+
12
+ import type {
13
+ RawBalances,
14
+ Route,
15
+ StrategyRoute,
16
+ } from '../interfaces/IStrategy.js';
17
+ import { extractBridgeConfigs } from '../test/helpers.js';
18
+
19
+ import { CollateralDeficitStrategy } from './CollateralDeficitStrategy.js';
20
+
21
+ const testLogger = pino({ level: 'silent' });
22
+
23
+ const BRIDGE1 = '0x1234567890123456789012345678901234567890' as Address;
24
+ const BRIDGE2 = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as Address;
25
+ const OTHER_BRIDGE = '0x9876543210987654321098765432109876543210' as Address;
26
+
27
+ describe('CollateralDeficitStrategy', () => {
28
+ let chain1: ChainName;
29
+ let chain2: ChainName;
30
+ let chain3: ChainName;
31
+ const tokensByChainName: ChainMap<Token> = {};
32
+ const tokenArgs = {
33
+ name: 'USDC',
34
+ decimals: 6, // USDC has 6 decimals
35
+ symbol: 'USDC',
36
+ standard: TokenStandard.ERC20,
37
+ addressOrDenom: '',
38
+ };
39
+
40
+ beforeEach(() => {
41
+ chain1 = 'chain1';
42
+ chain2 = 'chain2';
43
+ chain3 = 'chain3';
44
+ tokensByChainName[chain1] = new Token({ ...tokenArgs, chainName: chain1 });
45
+ tokensByChainName[chain2] = new Token({ ...tokenArgs, chainName: chain2 });
46
+ tokensByChainName[chain3] = new Token({ ...tokenArgs, chainName: chain3 });
47
+ });
48
+
49
+ describe('constructor', () => {
50
+ it('should throw an error when less than two chains are configured', () => {
51
+ expect(
52
+ () =>
53
+ new CollateralDeficitStrategy(
54
+ {
55
+ [chain1]: {
56
+ bridge: BRIDGE1,
57
+ buffer: '1000',
58
+ },
59
+ },
60
+ tokensByChainName,
61
+ testLogger,
62
+ {},
63
+ ),
64
+ ).to.throw('At least two chains must be configured');
65
+ });
66
+
67
+ it('should create a strategy with valid config', () => {
68
+ const strategy = new CollateralDeficitStrategy(
69
+ {
70
+ [chain1]: {
71
+ bridge: BRIDGE1,
72
+ buffer: '1000',
73
+ },
74
+ [chain2]: {
75
+ bridge: BRIDGE2,
76
+ buffer: '500',
77
+ },
78
+ },
79
+ tokensByChainName,
80
+ testLogger,
81
+ {},
82
+ );
83
+ expect(strategy).to.be.instanceOf(CollateralDeficitStrategy);
84
+ });
85
+ });
86
+
87
+ describe('getCategorizedBalances', () => {
88
+ it('should detect deficit when balance is negative and add buffer', () => {
89
+ const strategy = new CollateralDeficitStrategy(
90
+ {
91
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
92
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
93
+ },
94
+ tokensByChainName,
95
+ testLogger,
96
+ {},
97
+ );
98
+
99
+ const rawBalances: RawBalances = {
100
+ [chain1]: -5_000_000n, // -5 USDC (6 decimals)
101
+ [chain2]: 10_000_000n, // 10 USDC
102
+ };
103
+
104
+ const result = strategy['getCategorizedBalances'](rawBalances);
105
+
106
+ // chain1: deficit = |-5 USDC| + 1000 USDC = 1005 USDC = 1005000000 (wei)
107
+ expect(result.deficits).to.have.lengthOf(1);
108
+ expect(result.deficits[0].chain).to.equal(chain1);
109
+ expect(result.deficits[0].amount).to.equal(1_005_000_000n);
110
+
111
+ // chain2: surplus = 10 USDC
112
+ expect(result.surpluses).to.have.lengthOf(1);
113
+ expect(result.surpluses[0].chain).to.equal(chain2);
114
+ expect(result.surpluses[0].amount).to.equal(10_000_000n);
115
+ });
116
+
117
+ it('should treat zero balance as neither surplus nor deficit', () => {
118
+ const strategy = new CollateralDeficitStrategy(
119
+ {
120
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
121
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
122
+ },
123
+ tokensByChainName,
124
+ testLogger,
125
+ {},
126
+ );
127
+
128
+ const rawBalances: RawBalances = {
129
+ [chain1]: 0n,
130
+ [chain2]: 10_000_000n,
131
+ };
132
+
133
+ const result = strategy['getCategorizedBalances'](rawBalances);
134
+
135
+ expect(result.deficits).to.have.lengthOf(0);
136
+ expect(result.surpluses).to.have.lengthOf(1);
137
+ expect(result.surpluses[0].chain).to.equal(chain2);
138
+ });
139
+
140
+ it('should treat positive balance as surplus', () => {
141
+ const strategy = new CollateralDeficitStrategy(
142
+ {
143
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
144
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
145
+ },
146
+ tokensByChainName,
147
+ testLogger,
148
+ {},
149
+ );
150
+
151
+ const rawBalances: RawBalances = {
152
+ [chain1]: 5_000_000n, // 5 USDC
153
+ [chain2]: 10_000_000n, // 10 USDC
154
+ };
155
+
156
+ const result = strategy['getCategorizedBalances'](rawBalances);
157
+
158
+ expect(result.deficits).to.have.lengthOf(0);
159
+ expect(result.surpluses).to.have.lengthOf(2);
160
+ });
161
+
162
+ it('should filter pending rebalances by configured bridges and simulate', () => {
163
+ const config = {
164
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
165
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
166
+ };
167
+ const bridgeConfigs = extractBridgeConfigs(config);
168
+
169
+ const strategy = new CollateralDeficitStrategy(
170
+ config,
171
+ tokensByChainName,
172
+ testLogger,
173
+ bridgeConfigs,
174
+ );
175
+
176
+ const rawBalances: RawBalances = {
177
+ [chain1]: -10_000_000n, // -10 USDC before simulation
178
+ [chain2]: 20_000_000n, // 20 USDC
179
+ };
180
+
181
+ const pendingRebalances: StrategyRoute[] = [
182
+ {
183
+ origin: chain2,
184
+ destination: chain1,
185
+ amount: 5_000_000n, // 5 USDC pending to chain1
186
+ bridge: BRIDGE2, // Matches chain2's bridge for chain2->chain1 route
187
+ },
188
+ ];
189
+
190
+ const result = strategy['getCategorizedBalances'](
191
+ rawBalances,
192
+ pendingRebalances,
193
+ );
194
+
195
+ // After simulation: chain1 = -10 + 5 = -5 USDC (destination increase)
196
+ // Deficit = |-5| + 1000 = 1005 USDC
197
+ expect(result.deficits).to.have.lengthOf(1);
198
+ expect(result.deficits[0].chain).to.equal(chain1);
199
+ expect(result.deficits[0].amount).to.equal(1_005_000_000n);
200
+
201
+ // chain2 after simulation: 20 USDC (no change - origin already deducted on-chain)
202
+ // Simulation only adds to destination, doesn't subtract from origin
203
+ expect(result.surpluses).to.have.lengthOf(1);
204
+ expect(result.surpluses[0].chain).to.equal(chain2);
205
+ expect(result.surpluses[0].amount).to.equal(20_000_000n);
206
+ });
207
+
208
+ it('should filter out pending rebalances with different bridge', () => {
209
+ const config = {
210
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
211
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
212
+ };
213
+ const bridgeConfigs = extractBridgeConfigs(config);
214
+
215
+ const strategy = new CollateralDeficitStrategy(
216
+ config,
217
+ tokensByChainName,
218
+ testLogger,
219
+ bridgeConfigs,
220
+ );
221
+
222
+ const rawBalances: RawBalances = {
223
+ [chain1]: -10_000_000n,
224
+ [chain2]: 20_000_000n,
225
+ };
226
+
227
+ const pendingRebalances: StrategyRoute[] = [
228
+ {
229
+ origin: chain2,
230
+ destination: chain1,
231
+ amount: 5_000_000n,
232
+ bridge: OTHER_BRIDGE, // Does NOT match chain2's configured bridge for chain2->chain1
233
+ },
234
+ ];
235
+
236
+ const result = strategy['getCategorizedBalances'](
237
+ rawBalances,
238
+ pendingRebalances,
239
+ );
240
+
241
+ // Pending rebalance should be filtered out, so no simulation
242
+ // Deficit = |-10| + 1000 = 1010 USDC
243
+ expect(result.deficits).to.have.lengthOf(1);
244
+ expect(result.deficits[0].amount).to.equal(1_010_000_000n);
245
+
246
+ // chain2: no subtraction, stays at 20 USDC
247
+ expect(result.surpluses[0].amount).to.equal(20_000_000n);
248
+ });
249
+
250
+ it('should handle pending rebalance that fully covers deficit', () => {
251
+ const config = {
252
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
253
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
254
+ };
255
+ const bridgeConfigs = extractBridgeConfigs(config);
256
+
257
+ const strategy = new CollateralDeficitStrategy(
258
+ config,
259
+ tokensByChainName,
260
+ testLogger,
261
+ bridgeConfigs,
262
+ );
263
+
264
+ const rawBalances: RawBalances = {
265
+ [chain1]: -5_000_000n, // -5 USDC
266
+ [chain2]: 20_000_000n,
267
+ };
268
+
269
+ const pendingRebalances: StrategyRoute[] = [
270
+ {
271
+ origin: chain2,
272
+ destination: chain1,
273
+ amount: 10_000_000n, // 10 USDC pending - more than enough
274
+ bridge: BRIDGE2, // Matches chain2's configured bridge for chain2->chain1
275
+ },
276
+ ];
277
+
278
+ const result = strategy['getCategorizedBalances'](
279
+ rawBalances,
280
+ pendingRebalances,
281
+ );
282
+
283
+ // After simulation: chain1 = -5 + 10 = 5 USDC (positive, no deficit)
284
+ expect(result.deficits).to.have.lengthOf(0);
285
+ expect(result.surpluses).to.have.lengthOf(2); // Both chains have surplus
286
+ });
287
+
288
+ it('should handle multiple chains with mixed balances', () => {
289
+ const strategy = new CollateralDeficitStrategy(
290
+ {
291
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
292
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
293
+ [chain3]: { bridge: BRIDGE1, buffer: '2000' },
294
+ },
295
+ tokensByChainName,
296
+ testLogger,
297
+ {},
298
+ );
299
+
300
+ const rawBalances: RawBalances = {
301
+ [chain1]: -5_000_000n, // -5 USDC -> deficit
302
+ [chain2]: 10_000_000n, // 10 USDC -> surplus
303
+ [chain3]: -3_000_000n, // -3 USDC -> deficit
304
+ };
305
+
306
+ const result = strategy['getCategorizedBalances'](rawBalances);
307
+
308
+ expect(result.deficits).to.have.lengthOf(2);
309
+ expect(result.surpluses).to.have.lengthOf(1);
310
+
311
+ // chain1: deficit = 5 + 1000 = 1005 USDC
312
+ const chain1Deficit = result.deficits.find((d) => d.chain === chain1);
313
+ expect(chain1Deficit?.amount).to.equal(1_005_000_000n);
314
+
315
+ // chain3: deficit = 3 + 2000 = 2003 USDC
316
+ const chain3Deficit = result.deficits.find((d) => d.chain === chain3);
317
+ expect(chain3Deficit?.amount).to.equal(2_003_000_000n);
318
+ });
319
+
320
+ it('should handle empty pending rebalances array', () => {
321
+ const strategy = new CollateralDeficitStrategy(
322
+ {
323
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
324
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
325
+ },
326
+ tokensByChainName,
327
+ testLogger,
328
+ {},
329
+ );
330
+
331
+ const rawBalances: RawBalances = {
332
+ [chain1]: -5_000_000n,
333
+ [chain2]: 10_000_000n,
334
+ };
335
+
336
+ const result = strategy['getCategorizedBalances'](rawBalances, []);
337
+
338
+ // No simulation should occur
339
+ expect(result.deficits).to.have.lengthOf(1);
340
+ expect(result.deficits[0].amount).to.equal(1_005_000_000n);
341
+ });
342
+ });
343
+
344
+ describe('getRebalancingRoutes', () => {
345
+ it('should set bridge field on output routes', () => {
346
+ const config = {
347
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
348
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
349
+ };
350
+ const bridgeConfigs = extractBridgeConfigs(config);
351
+
352
+ const strategy = new CollateralDeficitStrategy(
353
+ config,
354
+ tokensByChainName,
355
+ testLogger,
356
+ bridgeConfigs,
357
+ );
358
+
359
+ // Start with positive balances
360
+ const rawBalances: RawBalances = {
361
+ [chain1]: 2_000_000n, // 2 USDC
362
+ [chain2]: 20_000_000n, // 20 USDC
363
+ };
364
+
365
+ // Pending transfer will drain chain1 to create deficit
366
+ const inflightContext = {
367
+ pendingTransfers: [
368
+ {
369
+ origin: chain2,
370
+ destination: chain1,
371
+ amount: 7_000_000n, // 7 USDC pending to chain1
372
+ },
373
+ ],
374
+ pendingRebalances: [] as StrategyRoute[],
375
+ };
376
+
377
+ // After reserveCollateral: chain1 = 2 - 7 = -5 USDC (deficit)
378
+ const routes = strategy.getRebalancingRoutes(
379
+ rawBalances,
380
+ inflightContext,
381
+ );
382
+
383
+ expect(routes).to.have.lengthOf(1);
384
+ expect(routes[0].origin).to.equal(chain2);
385
+ expect(routes[0].destination).to.equal(chain1);
386
+ expect(routes[0].bridge).to.equal(BRIDGE2); // Uses chain2's (origin) bridge
387
+ });
388
+
389
+ it('should generate routes from surplus to deficit chains', () => {
390
+ const config = {
391
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
392
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
393
+ [chain3]: { bridge: BRIDGE1, buffer: '100' },
394
+ };
395
+ const bridgeConfigs = extractBridgeConfigs(config);
396
+
397
+ const strategy = new CollateralDeficitStrategy(
398
+ config,
399
+ tokensByChainName,
400
+ testLogger,
401
+ bridgeConfigs,
402
+ );
403
+
404
+ // Start with positive balances
405
+ const rawBalances: RawBalances = {
406
+ [chain1]: 5_000_000n, // 5 USDC
407
+ [chain2]: 20_000_000n, // 20 USDC
408
+ [chain3]: 5_000_000n, // 5 USDC
409
+ };
410
+
411
+ // Pending transfer will create deficit on chain1
412
+ const inflightContext = {
413
+ pendingTransfers: [
414
+ {
415
+ origin: chain2,
416
+ destination: chain1,
417
+ amount: 15_000_000n, // 15 USDC pending to chain1
418
+ },
419
+ ],
420
+ pendingRebalances: [] as StrategyRoute[],
421
+ };
422
+
423
+ // After reserveCollateral: chain1 = 5 - 15 = -10 USDC (deficit)
424
+ const routes = strategy.getRebalancingRoutes(
425
+ rawBalances,
426
+ inflightContext,
427
+ );
428
+
429
+ // Should have route(s) from surplus chains (chain2, chain3) to deficit chain (chain1)
430
+ expect(routes.length).to.be.greaterThan(0);
431
+ routes.forEach((route) => {
432
+ expect([chain2, chain3]).to.include(route.origin);
433
+ expect(route.destination).to.equal(chain1);
434
+ });
435
+ });
436
+ });
437
+
438
+ describe('filterByConfiguredBridges', () => {
439
+ it('should filter rebalances by configured bridge for route', () => {
440
+ const config = {
441
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
442
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
443
+ };
444
+ const bridgeConfigs = extractBridgeConfigs(config);
445
+
446
+ const strategy = new CollateralDeficitStrategy(
447
+ config,
448
+ tokensByChainName,
449
+ testLogger,
450
+ bridgeConfigs,
451
+ );
452
+
453
+ const pendingRebalances: Array<Route & { bridge?: Address }> = [
454
+ {
455
+ origin: chain2,
456
+ destination: chain1,
457
+ amount: 5_000_000n,
458
+ bridge: BRIDGE2,
459
+ },
460
+ {
461
+ origin: chain1,
462
+ destination: chain2,
463
+ amount: 3_000_000n,
464
+ bridge: OTHER_BRIDGE,
465
+ },
466
+ {
467
+ origin: chain2,
468
+ destination: chain1,
469
+ amount: 2_000_000n,
470
+ },
471
+ ];
472
+
473
+ const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
474
+
475
+ expect(filtered).to.have.lengthOf(2);
476
+ expect((filtered[0] as StrategyRoute).bridge).to.equal(BRIDGE2);
477
+ expect((filtered[1] as StrategyRoute).bridge).to.be.undefined;
478
+ });
479
+
480
+ it('should include rebalance when bridge matches configured bridge for the route', () => {
481
+ const config = {
482
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
483
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
484
+ };
485
+ const bridgeConfigs = extractBridgeConfigs(config);
486
+
487
+ const strategy = new CollateralDeficitStrategy(
488
+ config,
489
+ tokensByChainName,
490
+ testLogger,
491
+ bridgeConfigs,
492
+ );
493
+
494
+ const pendingRebalances: Array<Route & { bridge?: Address }> = [
495
+ {
496
+ origin: chain2,
497
+ destination: chain1,
498
+ amount: 5_000_000n,
499
+ bridge: BRIDGE2,
500
+ },
501
+ ];
502
+
503
+ const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
504
+ expect(filtered).to.have.lengthOf(1);
505
+ expect((filtered[0] as StrategyRoute).bridge).to.equal(BRIDGE2);
506
+ });
507
+
508
+ it('should exclude rebalance when bridge does not match configured bridge for the route', () => {
509
+ const config = {
510
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
511
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
512
+ };
513
+ const bridgeConfigs = extractBridgeConfigs(config);
514
+
515
+ const strategy = new CollateralDeficitStrategy(
516
+ config,
517
+ tokensByChainName,
518
+ testLogger,
519
+ bridgeConfigs,
520
+ );
521
+
522
+ // Route from chain2 → chain1 with a different bridge
523
+ const pendingRebalances: StrategyRoute[] = [
524
+ {
525
+ origin: chain2,
526
+ destination: chain1,
527
+ amount: 5_000_000n,
528
+ bridge: BRIDGE1, // Does NOT match configured bridge for chain2->chain1 (should be BRIDGE2)
529
+ },
530
+ ];
531
+
532
+ const filtered = strategy['filterByConfiguredBridges'](pendingRebalances);
533
+ expect(filtered).to.have.lengthOf(0);
534
+ });
535
+
536
+ it('should return empty array for undefined pending rebalances', () => {
537
+ const strategy = new CollateralDeficitStrategy(
538
+ {
539
+ [chain1]: { bridge: BRIDGE1, buffer: '1000' },
540
+ [chain2]: { bridge: BRIDGE2, buffer: '500' },
541
+ },
542
+ tokensByChainName,
543
+ testLogger,
544
+ {},
545
+ );
546
+
547
+ const filtered = strategy['filterByConfiguredBridges'](undefined);
548
+ expect(filtered).to.have.lengthOf(0);
549
+ });
550
+ });
551
+ });