@hyperlane-xyz/rebalancer 2.0.0 → 3.1.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 (213) hide show
  1. package/dist/bridges/LiFiBridge.d.ts +67 -0
  2. package/dist/bridges/LiFiBridge.d.ts.map +1 -0
  3. package/dist/bridges/LiFiBridge.js +386 -0
  4. package/dist/bridges/LiFiBridge.js.map +1 -0
  5. package/dist/config/RebalancerConfig.d.ts +8 -2
  6. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  7. package/dist/config/RebalancerConfig.js +9 -4
  8. package/dist/config/RebalancerConfig.js.map +1 -1
  9. package/dist/config/RebalancerConfig.test.js +135 -1
  10. package/dist/config/RebalancerConfig.test.js.map +1 -1
  11. package/dist/config/types.d.ts +1023 -304
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +113 -10
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/core/InventoryRebalancer.d.ts +190 -0
  16. package/dist/core/InventoryRebalancer.d.ts.map +1 -0
  17. package/dist/core/InventoryRebalancer.js +892 -0
  18. package/dist/core/InventoryRebalancer.js.map +1 -0
  19. package/dist/core/InventoryRebalancer.test.d.ts +2 -0
  20. package/dist/core/InventoryRebalancer.test.d.ts.map +1 -0
  21. package/dist/core/InventoryRebalancer.test.js +1382 -0
  22. package/dist/core/InventoryRebalancer.test.js.map +1 -0
  23. package/dist/core/Rebalancer.d.ts +11 -4
  24. package/dist/core/Rebalancer.d.ts.map +1 -1
  25. package/dist/core/Rebalancer.js +92 -9
  26. package/dist/core/Rebalancer.js.map +1 -1
  27. package/dist/core/Rebalancer.test.js +82 -49
  28. package/dist/core/Rebalancer.test.js.map +1 -1
  29. package/dist/core/RebalancerOrchestrator.d.ts +30 -9
  30. package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
  31. package/dist/core/RebalancerOrchestrator.js +79 -71
  32. package/dist/core/RebalancerOrchestrator.js.map +1 -1
  33. package/dist/core/RebalancerOrchestrator.test.d.ts +2 -0
  34. package/dist/core/RebalancerOrchestrator.test.d.ts.map +1 -0
  35. package/dist/core/RebalancerOrchestrator.test.js +719 -0
  36. package/dist/core/RebalancerOrchestrator.test.js.map +1 -0
  37. package/dist/core/RebalancerService.d.ts +7 -3
  38. package/dist/core/RebalancerService.d.ts.map +1 -1
  39. package/dist/core/RebalancerService.js +44 -24
  40. package/dist/core/RebalancerService.js.map +1 -1
  41. package/dist/core/RebalancerService.test.js +74 -110
  42. package/dist/core/RebalancerService.test.js.map +1 -1
  43. package/dist/e2e/collateral-deficit.e2e-test.js +1 -3
  44. package/dist/e2e/collateral-deficit.e2e-test.js.map +1 -1
  45. package/dist/e2e/composite.e2e-test.js.map +1 -1
  46. package/dist/e2e/harness/BridgeSetup.d.ts +6 -0
  47. package/dist/e2e/harness/BridgeSetup.d.ts.map +1 -1
  48. package/dist/e2e/harness/BridgeSetup.js +10 -1
  49. package/dist/e2e/harness/BridgeSetup.js.map +1 -1
  50. package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
  51. package/dist/e2e/harness/ForkIndexer.js +1 -0
  52. package/dist/e2e/harness/ForkIndexer.js.map +1 -1
  53. package/dist/e2e/harness/TestHelpers.d.ts.map +1 -1
  54. package/dist/e2e/harness/TestHelpers.js +1 -4
  55. package/dist/e2e/harness/TestHelpers.js.map +1 -1
  56. package/dist/e2e/harness/TestRebalancer.d.ts +1 -1
  57. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  58. package/dist/e2e/harness/TestRebalancer.js +9 -9
  59. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  60. package/dist/e2e/minAmount.e2e-test.js +0 -1
  61. package/dist/e2e/minAmount.e2e-test.js.map +1 -1
  62. package/dist/e2e/weighted.e2e-test.js +0 -1
  63. package/dist/e2e/weighted.e2e-test.js.map +1 -1
  64. package/dist/factories/RebalancerContextFactory.d.ts +48 -6
  65. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  66. package/dist/factories/RebalancerContextFactory.js +171 -17
  67. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  68. package/dist/index.d.ts +6 -6
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/interfaces/IExternalBridge.d.ts +101 -0
  73. package/dist/interfaces/IExternalBridge.d.ts.map +1 -0
  74. package/dist/interfaces/IExternalBridge.js +2 -0
  75. package/dist/interfaces/IExternalBridge.js.map +1 -0
  76. package/dist/interfaces/IMonitor.d.ts +1 -0
  77. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  78. package/dist/interfaces/IRebalancer.d.ts +25 -25
  79. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  80. package/dist/interfaces/IStrategy.d.ts +36 -3
  81. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  82. package/dist/interfaces/IStrategy.js +12 -1
  83. package/dist/interfaces/IStrategy.js.map +1 -1
  84. package/dist/metrics/PriceGetter.js +1 -1
  85. package/dist/metrics/PriceGetter.js.map +1 -1
  86. package/dist/metrics/scripts/metrics.d.ts +3 -3
  87. package/dist/monitor/Monitor.d.ts +12 -2
  88. package/dist/monitor/Monitor.d.ts.map +1 -1
  89. package/dist/monitor/Monitor.js +46 -1
  90. package/dist/monitor/Monitor.js.map +1 -1
  91. package/dist/service.js +40 -17
  92. package/dist/service.js.map +1 -1
  93. package/dist/strategy/BaseStrategy.d.ts +12 -6
  94. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  95. package/dist/strategy/BaseStrategy.js +56 -21
  96. package/dist/strategy/BaseStrategy.js.map +1 -1
  97. package/dist/strategy/CollateralDeficitStrategy.d.ts +1 -1
  98. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
  99. package/dist/strategy/CollateralDeficitStrategy.js +19 -11
  100. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
  101. package/dist/strategy/CollateralDeficitStrategy.test.js +135 -2
  102. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -1
  103. package/dist/strategy/CompositeStrategy.test.js +13 -0
  104. package/dist/strategy/CompositeStrategy.test.js.map +1 -1
  105. package/dist/strategy/MinAmountStrategy.test.js +4 -0
  106. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  107. package/dist/strategy/StrategyFactory.d.ts +2 -1
  108. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  109. package/dist/strategy/StrategyFactory.js +24 -8
  110. package/dist/strategy/StrategyFactory.js.map +1 -1
  111. package/dist/strategy/WeightedStrategy.test.js +6 -0
  112. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  113. package/dist/test/helpers.d.ts +8 -7
  114. package/dist/test/helpers.d.ts.map +1 -1
  115. package/dist/test/helpers.js +23 -5
  116. package/dist/test/helpers.js.map +1 -1
  117. package/dist/test/lifiMocks.d.ts +51 -0
  118. package/dist/test/lifiMocks.d.ts.map +1 -0
  119. package/dist/test/lifiMocks.js +130 -0
  120. package/dist/test/lifiMocks.js.map +1 -0
  121. package/dist/tracking/ActionTracker.d.ts +34 -1
  122. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  123. package/dist/tracking/ActionTracker.js +233 -26
  124. package/dist/tracking/ActionTracker.js.map +1 -1
  125. package/dist/tracking/ActionTracker.test.js +380 -19
  126. package/dist/tracking/ActionTracker.test.js.map +1 -1
  127. package/dist/tracking/IActionTracker.d.ts +48 -3
  128. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  129. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
  130. package/dist/tracking/InflightContextAdapter.js +24 -7
  131. package/dist/tracking/InflightContextAdapter.js.map +1 -1
  132. package/dist/tracking/InflightContextAdapter.test.js +7 -4
  133. package/dist/tracking/InflightContextAdapter.test.js.map +1 -1
  134. package/dist/tracking/types.d.ts +33 -2
  135. package/dist/tracking/types.d.ts.map +1 -1
  136. package/dist/utils/ExplorerClient.d.ts +3 -1
  137. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  138. package/dist/utils/ExplorerClient.js +16 -8
  139. package/dist/utils/ExplorerClient.js.map +1 -1
  140. package/dist/utils/bridgeUtils.d.ts +27 -4
  141. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  142. package/dist/utils/bridgeUtils.js +38 -0
  143. package/dist/utils/bridgeUtils.js.map +1 -1
  144. package/dist/utils/bridgeUtils.test.js +9 -0
  145. package/dist/utils/bridgeUtils.test.js.map +1 -1
  146. package/dist/utils/gasEstimation.d.ts +65 -0
  147. package/dist/utils/gasEstimation.d.ts.map +1 -0
  148. package/dist/utils/gasEstimation.js +176 -0
  149. package/dist/utils/gasEstimation.js.map +1 -0
  150. package/dist/utils/tokenUtils.d.ts +9 -1
  151. package/dist/utils/tokenUtils.d.ts.map +1 -1
  152. package/dist/utils/tokenUtils.js +11 -0
  153. package/dist/utils/tokenUtils.js.map +1 -1
  154. package/package.json +9 -7
  155. package/src/bridges/LiFiBridge.ts +538 -0
  156. package/src/config/RebalancerConfig.test.ts +162 -0
  157. package/src/config/RebalancerConfig.ts +21 -3
  158. package/src/config/types.ts +147 -10
  159. package/src/core/InventoryRebalancer.test.ts +1721 -0
  160. package/src/core/InventoryRebalancer.ts +1265 -0
  161. package/src/core/Rebalancer.test.ts +84 -30
  162. package/src/core/Rebalancer.ts +144 -23
  163. package/src/core/RebalancerOrchestrator.test.ts +869 -0
  164. package/src/core/RebalancerOrchestrator.ts +146 -95
  165. package/src/core/RebalancerService.test.ts +86 -124
  166. package/src/core/RebalancerService.ts +67 -33
  167. package/src/e2e/collateral-deficit.e2e-test.ts +2 -4
  168. package/src/e2e/composite.e2e-test.ts +5 -5
  169. package/src/e2e/harness/BridgeSetup.ts +28 -1
  170. package/src/e2e/harness/ForkIndexer.ts +1 -0
  171. package/src/e2e/harness/TestHelpers.ts +1 -4
  172. package/src/e2e/harness/TestRebalancer.ts +10 -7
  173. package/src/e2e/minAmount.e2e-test.ts +1 -2
  174. package/src/e2e/weighted.e2e-test.ts +1 -2
  175. package/src/factories/RebalancerContextFactory.ts +294 -24
  176. package/src/index.ts +22 -5
  177. package/src/interfaces/IExternalBridge.ts +115 -0
  178. package/src/interfaces/IMonitor.ts +1 -0
  179. package/src/interfaces/IRebalancer.ts +45 -29
  180. package/src/interfaces/IStrategy.ts +50 -3
  181. package/src/metrics/PriceGetter.ts +1 -1
  182. package/src/monitor/Monitor.ts +81 -2
  183. package/src/service.ts +59 -18
  184. package/src/strategy/BaseStrategy.ts +77 -24
  185. package/src/strategy/CollateralDeficitStrategy.test.ts +181 -4
  186. package/src/strategy/CollateralDeficitStrategy.ts +42 -15
  187. package/src/strategy/CompositeStrategy.test.ts +13 -0
  188. package/src/strategy/MinAmountStrategy.test.ts +4 -0
  189. package/src/strategy/StrategyFactory.ts +33 -6
  190. package/src/strategy/WeightedStrategy.test.ts +6 -0
  191. package/src/test/helpers.ts +39 -14
  192. package/src/test/lifiMocks.ts +174 -0
  193. package/src/tracking/ActionTracker.test.ts +443 -19
  194. package/src/tracking/ActionTracker.ts +339 -28
  195. package/src/tracking/IActionTracker.ts +59 -3
  196. package/src/tracking/InflightContextAdapter.test.ts +7 -4
  197. package/src/tracking/InflightContextAdapter.ts +42 -9
  198. package/src/tracking/types.ts +45 -2
  199. package/src/utils/ExplorerClient.ts +27 -10
  200. package/src/utils/bridgeUtils.test.ts +9 -0
  201. package/src/utils/bridgeUtils.ts +75 -6
  202. package/src/utils/gasEstimation.ts +272 -0
  203. package/src/utils/tokenUtils.ts +12 -0
  204. package/dist/tracking/index.d.ts +0 -7
  205. package/dist/tracking/index.d.ts.map +0 -1
  206. package/dist/tracking/index.js +0 -6
  207. package/dist/tracking/index.js.map +0 -1
  208. package/dist/utils/index.d.ts +0 -5
  209. package/dist/utils/index.d.ts.map +0 -1
  210. package/dist/utils/index.js +0 -5
  211. package/dist/utils/index.js.map +0 -1
  212. package/src/tracking/index.ts +0 -36
  213. package/src/utils/index.ts +0 -4
@@ -7,12 +7,49 @@ import Sinon from 'sinon';
7
7
  import { HyperlaneCore } from '@hyperlane-xyz/sdk';
8
8
 
9
9
  import {
10
- buildTestRebalanceRoute,
10
+ buildTestMovableCollateralRoute,
11
11
  createRebalancerTestContext,
12
12
  } from '../test/helpers.js';
13
+ import type { IActionTracker } from '../tracking/IActionTracker.js';
13
14
 
14
15
  import { Rebalancer } from './Rebalancer.js';
15
16
 
17
+ function createMockActionTracker(): IActionTracker {
18
+ return {
19
+ initialize: Sinon.stub().resolves(),
20
+ createRebalanceIntent: Sinon.stub().callsFake(async () => ({
21
+ id: `intent-${Date.now()}`,
22
+ status: 'not_started',
23
+ })),
24
+ createRebalanceAction: Sinon.stub().resolves(),
25
+ completeRebalanceAction: Sinon.stub().resolves(),
26
+ failRebalanceAction: Sinon.stub().resolves(),
27
+ completeRebalanceIntent: Sinon.stub().resolves(),
28
+ cancelRebalanceIntent: Sinon.stub().resolves(),
29
+ failRebalanceIntent: Sinon.stub().resolves(),
30
+ syncTransfers: Sinon.stub().resolves(),
31
+ syncRebalanceIntents: Sinon.stub().resolves(),
32
+ syncRebalanceActions: Sinon.stub().resolves(),
33
+ syncInventoryMovementActions: Sinon.stub().resolves({
34
+ completed: 0,
35
+ failed: 0,
36
+ }),
37
+ logStoreContents: Sinon.stub().resolves(),
38
+ getInProgressTransfers: Sinon.stub().resolves([]),
39
+ getActiveRebalanceIntents: Sinon.stub().resolves([]),
40
+ getTransfersByDestination: Sinon.stub().resolves([]),
41
+ getRebalanceIntentsByDestination: Sinon.stub().resolves([]),
42
+ getTransfer: Sinon.stub().resolves(undefined),
43
+ getRebalanceIntent: Sinon.stub().resolves(undefined),
44
+ getRebalanceAction: Sinon.stub().resolves(undefined),
45
+ getInProgressActions: Sinon.stub().resolves([]),
46
+ getPartiallyFulfilledInventoryIntents: Sinon.stub().resolves([]),
47
+ getActionsByType: Sinon.stub().resolves([]),
48
+ getActionsForIntent: Sinon.stub().resolves([]),
49
+ getInflightInventoryMovements: Sinon.stub().resolves(0n),
50
+ };
51
+ }
52
+
16
53
  chai.use(chaiAsPromised);
17
54
 
18
55
  const testLogger = pino({ level: 'silent' });
@@ -36,6 +73,7 @@ describe('Rebalancer', () => {
36
73
  ctx.chainMetadata,
37
74
  ctx.tokensByChainName,
38
75
  ctx.multiProvider as any,
76
+ createMockActionTracker(),
39
77
  testLogger,
40
78
  );
41
79
 
@@ -58,19 +96,18 @@ describe('Rebalancer', () => {
58
96
  ctx.chainMetadata,
59
97
  ctx.tokensByChainName,
60
98
  ctx.multiProvider as any,
99
+ createMockActionTracker(),
61
100
  testLogger,
62
101
  );
63
102
 
64
- const route = buildTestRebalanceRoute({
103
+ const route = buildTestMovableCollateralRoute({
65
104
  origin: 'ethereum',
66
105
  destination: 'arbitrum',
67
106
  });
68
-
69
107
  const results = await rebalancer.rebalance([route]);
70
108
 
71
109
  expect(results).to.have.lengthOf(1);
72
110
  expect(results[0].success).to.be.true;
73
- expect(results[0].route).to.deep.equal(route);
74
111
  });
75
112
 
76
113
  it('should return failure results for routes that fail preparation', async () => {
@@ -83,15 +120,15 @@ describe('Rebalancer', () => {
83
120
  ctx.chainMetadata,
84
121
  ctx.tokensByChainName,
85
122
  ctx.multiProvider as any,
123
+ createMockActionTracker(),
86
124
  testLogger,
87
125
  );
88
126
 
89
- const route = buildTestRebalanceRoute();
127
+ const route = buildTestMovableCollateralRoute();
90
128
  const results = await rebalancer.rebalance([route]);
91
129
 
92
130
  expect(results).to.have.lengthOf(1);
93
131
  expect(results[0].success).to.be.false;
94
- expect(results[0].route).to.deep.equal(route);
95
132
  });
96
133
 
97
134
  it('should handle mixed success and failure results', async () => {
@@ -121,15 +158,16 @@ describe('Rebalancer', () => {
121
158
  },
122
159
  ctx.tokensByChainName,
123
160
  ctx.multiProvider as any,
161
+ createMockActionTracker(),
124
162
  testLogger,
125
163
  );
126
164
 
127
165
  const routes = [
128
- buildTestRebalanceRoute({
166
+ buildTestMovableCollateralRoute({
129
167
  origin: 'ethereum',
130
168
  destination: 'arbitrum',
131
169
  }),
132
- buildTestRebalanceRoute({
170
+ buildTestMovableCollateralRoute({
133
171
  origin: 'optimism',
134
172
  destination: 'arbitrum',
135
173
  }),
@@ -154,10 +192,11 @@ describe('Rebalancer', () => {
154
192
  ctx.chainMetadata,
155
193
  ctx.tokensByChainName,
156
194
  ctx.multiProvider as any,
195
+ createMockActionTracker(),
157
196
  testLogger,
158
197
  );
159
198
 
160
- const route = buildTestRebalanceRoute({
199
+ const route = buildTestMovableCollateralRoute({
161
200
  origin: 'ethereum',
162
201
  destination: 'arbitrum',
163
202
  });
@@ -176,10 +215,11 @@ describe('Rebalancer', () => {
176
215
  ctx.chainMetadata,
177
216
  ctx.tokensByChainName,
178
217
  ctx.multiProvider as any,
218
+ createMockActionTracker(),
179
219
  testLogger,
180
220
  );
181
221
 
182
- const route = buildTestRebalanceRoute({
222
+ const route = buildTestMovableCollateralRoute({
183
223
  origin: 'ethereum',
184
224
  destination: 'arbitrum',
185
225
  });
@@ -199,10 +239,11 @@ describe('Rebalancer', () => {
199
239
  ctx.chainMetadata,
200
240
  ctx.tokensByChainName,
201
241
  ctx.multiProvider as any,
242
+ createMockActionTracker(),
202
243
  testLogger,
203
244
  );
204
245
 
205
- const route = buildTestRebalanceRoute();
246
+ const route = buildTestMovableCollateralRoute();
206
247
  const results = await rebalancer.rebalance([route]);
207
248
 
208
249
  expect(results).to.have.lengthOf(1);
@@ -221,10 +262,11 @@ describe('Rebalancer', () => {
221
262
  ctx.chainMetadata,
222
263
  ctx.tokensByChainName,
223
264
  ctx.multiProvider as any,
265
+ createMockActionTracker(),
224
266
  testLogger,
225
267
  );
226
268
 
227
- const route = buildTestRebalanceRoute();
269
+ const route = buildTestMovableCollateralRoute();
228
270
  const results = await rebalancer.rebalance([route]);
229
271
 
230
272
  expect(results).to.have.lengthOf(1);
@@ -241,10 +283,11 @@ describe('Rebalancer', () => {
241
283
  ctx.chainMetadata,
242
284
  ctx.tokensByChainName,
243
285
  ctx.multiProvider as any,
286
+ createMockActionTracker(),
244
287
  testLogger,
245
288
  );
246
289
 
247
- const route = buildTestRebalanceRoute();
290
+ const route = buildTestMovableCollateralRoute();
248
291
  const results = await rebalancer.rebalance([route]);
249
292
 
250
293
  expect(results).to.have.lengthOf(1);
@@ -263,10 +306,11 @@ describe('Rebalancer', () => {
263
306
  ctx.chainMetadata,
264
307
  ctx.tokensByChainName,
265
308
  ctx.multiProvider as any,
309
+ createMockActionTracker(),
266
310
  testLogger,
267
311
  );
268
312
 
269
- const route = buildTestRebalanceRoute();
313
+ const route = buildTestMovableCollateralRoute();
270
314
  const results = await rebalancer.rebalance([route]);
271
315
 
272
316
  expect(results).to.have.lengthOf(1);
@@ -283,10 +327,11 @@ describe('Rebalancer', () => {
283
327
  ctx.chainMetadata,
284
328
  ctx.tokensByChainName,
285
329
  ctx.multiProvider as any,
330
+ createMockActionTracker(),
286
331
  testLogger,
287
332
  );
288
333
 
289
- const route = buildTestRebalanceRoute();
334
+ const route = buildTestMovableCollateralRoute();
290
335
  const results = await rebalancer.rebalance([route]);
291
336
 
292
337
  expect(results).to.have.lengthOf(1);
@@ -312,10 +357,11 @@ describe('Rebalancer', () => {
312
357
  ctx.chainMetadata,
313
358
  ctx.tokensByChainName,
314
359
  ctx.multiProvider as any,
360
+ createMockActionTracker(),
315
361
  testLogger,
316
362
  );
317
363
 
318
- const route = buildTestRebalanceRoute();
364
+ const route = buildTestMovableCollateralRoute();
319
365
  const results = await rebalancer.rebalance([route]);
320
366
 
321
367
  expect(results).to.have.lengthOf(1);
@@ -357,15 +403,16 @@ describe('Rebalancer', () => {
357
403
  },
358
404
  { ...ctx.tokensByChainName, optimism: ctx.tokensByChainName.ethereum },
359
405
  ctx.multiProvider as any,
406
+ createMockActionTracker(),
360
407
  testLogger,
361
408
  );
362
409
 
363
410
  const routes = [
364
- buildTestRebalanceRoute({
411
+ buildTestMovableCollateralRoute({
365
412
  origin: 'ethereum',
366
413
  destination: 'arbitrum',
367
414
  }),
368
- buildTestRebalanceRoute({
415
+ buildTestMovableCollateralRoute({
369
416
  origin: 'optimism',
370
417
  destination: 'arbitrum',
371
418
  }),
@@ -408,19 +455,20 @@ describe('Rebalancer', () => {
408
455
  ctx.chainMetadata,
409
456
  ctx.tokensByChainName,
410
457
  ctx.multiProvider as any,
458
+ createMockActionTracker(),
411
459
  testLogger,
412
460
  );
413
461
 
414
462
  const routes = [
415
- buildTestRebalanceRoute({
463
+ buildTestMovableCollateralRoute({
416
464
  origin: 'ethereum',
417
465
  destination: 'arbitrum',
418
466
  }),
419
- buildTestRebalanceRoute({
467
+ buildTestMovableCollateralRoute({
420
468
  origin: 'ethereum',
421
469
  destination: 'optimism',
422
470
  }),
423
- buildTestRebalanceRoute({
471
+ buildTestMovableCollateralRoute({
424
472
  origin: 'optimism',
425
473
  destination: 'arbitrum',
426
474
  }),
@@ -444,10 +492,11 @@ describe('Rebalancer', () => {
444
492
  ctx.chainMetadata,
445
493
  ctx.tokensByChainName,
446
494
  ctx.multiProvider as any,
495
+ createMockActionTracker(),
447
496
  testLogger,
448
497
  );
449
498
 
450
- const route = buildTestRebalanceRoute();
499
+ const route = buildTestMovableCollateralRoute();
451
500
  const results = await rebalancer.rebalance([route]);
452
501
 
453
502
  expect(results).to.have.lengthOf(1);
@@ -483,14 +532,15 @@ describe('Rebalancer', () => {
483
532
  ctx.chainMetadata,
484
533
  ctx.tokensByChainName,
485
534
  ctx.multiProvider as any,
535
+ createMockActionTracker(),
486
536
  testLogger,
487
537
  );
488
538
 
489
539
  const routes = [
490
- buildTestRebalanceRoute({
540
+ buildTestMovableCollateralRoute({
491
541
  amount: ethers.utils.parseEther('100').toBigInt(),
492
542
  }),
493
- buildTestRebalanceRoute({
543
+ buildTestMovableCollateralRoute({
494
544
  amount: ethers.utils.parseEther('200').toBigInt(),
495
545
  }),
496
546
  ];
@@ -533,16 +583,17 @@ describe('Rebalancer', () => {
533
583
  ctx.chainMetadata,
534
584
  ctx.tokensByChainName,
535
585
  ctx.multiProvider as any,
586
+ createMockActionTracker(),
536
587
  testLogger,
537
588
  );
538
589
 
539
590
  const routes = [
540
- buildTestRebalanceRoute({
591
+ buildTestMovableCollateralRoute({
541
592
  origin: 'ethereum',
542
593
  destination: 'arbitrum',
543
594
  amount: ethers.utils.parseEther('100').toBigInt(),
544
595
  }),
545
- buildTestRebalanceRoute({
596
+ buildTestMovableCollateralRoute({
546
597
  origin: 'ethereum',
547
598
  destination: 'optimism',
548
599
  amount: ethers.utils.parseEther('200').toBigInt(),
@@ -570,10 +621,11 @@ describe('Rebalancer', () => {
570
621
  ctx.chainMetadata,
571
622
  ctx.tokensByChainName,
572
623
  ctx.multiProvider as any,
624
+ createMockActionTracker(),
573
625
  testLogger,
574
626
  );
575
627
 
576
- const route = buildTestRebalanceRoute();
628
+ const route = buildTestMovableCollateralRoute();
577
629
  const results = await rebalancer.rebalance([route]);
578
630
 
579
631
  expect(results).to.have.lengthOf(1);
@@ -591,16 +643,17 @@ describe('Rebalancer', () => {
591
643
  ctx.chainMetadata,
592
644
  ctx.tokensByChainName,
593
645
  ctx.multiProvider as any,
646
+ createMockActionTracker(),
594
647
  testLogger,
595
648
  );
596
649
 
597
- const route = buildTestRebalanceRoute();
650
+ const route = buildTestMovableCollateralRoute();
598
651
  const results = await rebalancer.rebalance([route]);
599
652
 
600
653
  expect(results).to.have.lengthOf(1);
601
654
  expect(results[0].success).to.be.false;
602
655
  expect(results[0].error).to.include('no Dispatch event found');
603
- expect(results[0].messageId).to.be.undefined;
656
+ expect(results[0].messageId).to.equal('');
604
657
  });
605
658
 
606
659
  it('should include txHash in result', async () => {
@@ -619,10 +672,11 @@ describe('Rebalancer', () => {
619
672
  ctx.chainMetadata,
620
673
  ctx.tokensByChainName,
621
674
  ctx.multiProvider as any,
675
+ createMockActionTracker(),
622
676
  testLogger,
623
677
  );
624
678
 
625
- const route = buildTestRebalanceRoute();
679
+ const route = buildTestMovableCollateralRoute();
626
680
  const results = await rebalancer.rebalance([route]);
627
681
 
628
682
  expect(results).to.have.lengthOf(1);
@@ -16,20 +16,33 @@ import {
16
16
  import { eqAddress, isNullish, mapAllSettled } from '@hyperlane-xyz/utils';
17
17
 
18
18
  import type {
19
- IRebalancer,
19
+ IMovableCollateralRebalancer,
20
+ MovableCollateralExecutionResult,
20
21
  PreparedTransaction,
21
- RebalanceExecutionResult,
22
- RebalanceRoute,
22
+ RebalancerType,
23
23
  } from '../interfaces/IRebalancer.js';
24
+ import { MovableCollateralRoute } from '../interfaces/IStrategy.js';
24
25
  import { type Metrics } from '../metrics/Metrics.js';
26
+ import type { IActionTracker } from '../tracking/IActionTracker.js';
27
+ import type { RebalanceIntent } from '../tracking/types.js';
25
28
 
26
- export class Rebalancer implements IRebalancer {
29
+ // Internal types with intentId for tracking
30
+ type InternalExecutionResult = MovableCollateralExecutionResult & {
31
+ intentId: string;
32
+ };
33
+
34
+ type InternalRoute = MovableCollateralRoute & { intentId: string };
35
+
36
+ export class Rebalancer implements IMovableCollateralRebalancer {
37
+ public readonly rebalancerType: RebalancerType = 'movableCollateral';
27
38
  private readonly logger: Logger;
39
+
28
40
  constructor(
29
41
  private readonly warpCore: WarpCore,
30
42
  private readonly chainMetadata: ChainMap<ChainMetadata>,
31
43
  private readonly tokensByChainName: ChainMap<Token>,
32
44
  private readonly multiProvider: MultiProvider,
45
+ private readonly actionTracker: IActionTracker,
33
46
  logger: Logger,
34
47
  private readonly metrics?: Metrics,
35
48
  ) {
@@ -37,8 +50,8 @@ export class Rebalancer implements IRebalancer {
37
50
  }
38
51
 
39
52
  async rebalance(
40
- routes: RebalanceRoute[],
41
- ): Promise<RebalanceExecutionResult[]> {
53
+ routes: MovableCollateralRoute[],
54
+ ): Promise<MovableCollateralExecutionResult[]> {
42
55
  if (routes.length === 0) {
43
56
  this.logger.info('No routes to execute, exiting');
44
57
  return [];
@@ -46,20 +59,45 @@ export class Rebalancer implements IRebalancer {
46
59
 
47
60
  this.logger.info({ numberOfRoutes: routes.length }, 'Rebalance initiated');
48
61
 
62
+ const invalidRoutes = routes.filter((r) => !r.bridge);
63
+ if (invalidRoutes.length > 0) {
64
+ this.logger.error(
65
+ { count: invalidRoutes.length },
66
+ 'Routes missing required bridge address',
67
+ );
68
+ return routes.map((r) => ({
69
+ route: r,
70
+ success: false,
71
+ error: r.bridge ? undefined : 'Missing required bridge address',
72
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for validation failures
73
+ }));
74
+ }
75
+
76
+ const intents = await this.createIntents(routes);
77
+
78
+ const internalRoutes: InternalRoute[] = routes.map((route, idx) => ({
79
+ ...route,
80
+ bridge: route.bridge!,
81
+ intentId: intents[idx].id,
82
+ }));
83
+
49
84
  const { preparedTransactions, preparationFailureResults } =
50
- await this.prepareTransactions(routes);
85
+ await this.prepareTransactions(internalRoutes);
51
86
 
52
- let executionResults: RebalanceExecutionResult[] = [];
87
+ let executionResults: InternalExecutionResult[] = [];
53
88
 
54
89
  if (preparedTransactions.length > 0) {
55
90
  executionResults = await this.executeTransactions(preparedTransactions);
56
91
  }
57
92
 
58
- // Combine preparation failures with execution results
59
- const allResults = [...preparationFailureResults, ...executionResults];
93
+ const allInternalResults = [
94
+ ...preparationFailureResults,
95
+ ...executionResults,
96
+ ];
60
97
 
61
- // Record metrics for successful transactions
62
- const successfulResults = allResults.filter((r) => r.success);
98
+ await this.processResults(allInternalResults);
99
+
100
+ const successfulResults = allInternalResults.filter((r) => r.success);
63
101
  if (this.metrics && successfulResults.length > 0) {
64
102
  for (const result of successfulResults) {
65
103
  const token = this.tokensByChainName[result.route.origin];
@@ -72,22 +110,94 @@ export class Rebalancer implements IRebalancer {
72
110
  }
73
111
  }
74
112
 
75
- const failures = allResults.filter((r) => !r.success);
113
+ const failures = allInternalResults.filter((r) => !r.success);
76
114
  if (failures.length > 0) {
77
115
  this.logger.error(
78
116
  { failureCount: failures.length, totalRoutes: routes.length },
79
117
  'Some rebalance operations failed.',
80
118
  );
81
119
  } else {
82
- this.logger.info('Rebalance successful');
120
+ this.logger.info('Rebalance successful');
121
+ }
122
+
123
+ return this.toPublicResults(allInternalResults);
124
+ }
125
+
126
+ private async createIntents(
127
+ routes: MovableCollateralRoute[],
128
+ ): Promise<RebalanceIntent[]> {
129
+ return Promise.all(
130
+ routes.map((route) =>
131
+ this.actionTracker.createRebalanceIntent({
132
+ origin: this.multiProvider.getDomainId(route.origin),
133
+ destination: this.multiProvider.getDomainId(route.destination),
134
+ amount: route.amount,
135
+ bridge: route.bridge,
136
+ executionMethod: 'movable_collateral',
137
+ }),
138
+ ),
139
+ );
140
+ }
141
+
142
+ private async processResults(
143
+ results: InternalExecutionResult[],
144
+ ): Promise<void> {
145
+ for (const result of results) {
146
+ const intentId = result.intentId;
147
+
148
+ if (result.success && result.messageId) {
149
+ await this.actionTracker.createRebalanceAction({
150
+ intentId,
151
+ origin: this.multiProvider.getDomainId(result.route.origin),
152
+ destination: this.multiProvider.getDomainId(result.route.destination),
153
+ amount: result.route.amount,
154
+ type: 'rebalance_message',
155
+ messageId: result.messageId,
156
+ txHash: result.txHash,
157
+ });
158
+
159
+ this.logger.info(
160
+ {
161
+ intentId,
162
+ messageId: result.messageId,
163
+ txHash: result.txHash,
164
+ origin: result.route.origin,
165
+ destination: result.route.destination,
166
+ },
167
+ 'Rebalance action created successfully',
168
+ );
169
+ } else {
170
+ await this.actionTracker.failRebalanceIntent(intentId);
171
+
172
+ this.logger.warn(
173
+ {
174
+ intentId,
175
+ success: result.success,
176
+ error: result.error,
177
+ origin: result.route.origin,
178
+ destination: result.route.destination,
179
+ },
180
+ 'Rebalance intent marked as failed',
181
+ );
182
+ }
83
183
  }
184
+ }
84
185
 
85
- return allResults;
186
+ private toPublicResults(
187
+ internalResults: InternalExecutionResult[],
188
+ ): MovableCollateralExecutionResult[] {
189
+ return internalResults.map((internal) => ({
190
+ route: internal.route,
191
+ success: internal.success,
192
+ error: internal.error,
193
+ messageId: internal.messageId || '', // Ensure messageId is always a string
194
+ txHash: internal.txHash,
195
+ }));
86
196
  }
87
197
 
88
- private async prepareTransactions(routes: RebalanceRoute[]): Promise<{
198
+ private async prepareTransactions(routes: InternalRoute[]): Promise<{
89
199
  preparedTransactions: PreparedTransaction[];
90
- preparationFailureResults: RebalanceExecutionResult[];
200
+ preparationFailureResults: InternalExecutionResult[];
91
201
  }> {
92
202
  this.logger.info(
93
203
  { numRoutes: routes.length },
@@ -105,12 +215,14 @@ export class Rebalancer implements IRebalancer {
105
215
  );
106
216
 
107
217
  // Create failure results for tracking
108
- const preparationFailureResults: RebalanceExecutionResult[] = [];
218
+ const preparationFailureResults: InternalExecutionResult[] = [];
109
219
  for (const [i, error] of rejected) {
110
220
  preparationFailureResults.push({
111
221
  route: routes[i],
222
+ intentId: routes[i].intentId,
112
223
  success: false,
113
224
  error: String(error),
225
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for failures
114
226
  });
115
227
  }
116
228
  // Also track null results (validation failures)
@@ -118,8 +230,10 @@ export class Rebalancer implements IRebalancer {
118
230
  if (isNullish(tx)) {
119
231
  preparationFailureResults.push({
120
232
  route: routes[i],
233
+ intentId: routes[i].intentId,
121
234
  success: false,
122
235
  error: 'Preparation returned null',
236
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for failures
123
237
  });
124
238
  }
125
239
  });
@@ -128,7 +242,7 @@ export class Rebalancer implements IRebalancer {
128
242
  }
129
243
 
130
244
  private async prepareTransaction(
131
- route: RebalanceRoute,
245
+ route: InternalRoute,
132
246
  ): Promise<PreparedTransaction | null> {
133
247
  const { origin, destination, amount } = route;
134
248
 
@@ -209,7 +323,7 @@ export class Rebalancer implements IRebalancer {
209
323
  return { populatedTx, route, originTokenAmount };
210
324
  }
211
325
 
212
- private async validateRoute(route: RebalanceRoute): Promise<boolean> {
326
+ private async validateRoute(route: InternalRoute): Promise<boolean> {
213
327
  const { origin, destination, amount } = route;
214
328
  const originToken = this.tokensByChainName[origin];
215
329
  const destinationToken = this.tokensByChainName[destination];
@@ -322,13 +436,13 @@ export class Rebalancer implements IRebalancer {
322
436
 
323
437
  private async executeTransactions(
324
438
  transactions: PreparedTransaction[],
325
- ): Promise<RebalanceExecutionResult[]> {
439
+ ): Promise<InternalExecutionResult[]> {
326
440
  this.logger.info(
327
441
  { numTransactions: transactions.length },
328
442
  'Estimating gas for all prepared transactions.',
329
443
  );
330
444
 
331
- const results: RebalanceExecutionResult[] = [];
445
+ const results: InternalExecutionResult[] = [];
332
446
 
333
447
  // 1. Estimate gas for rebalance transactions
334
448
  const gasEstimateResults = await Promise.allSettled(
@@ -361,8 +475,10 @@ export class Rebalancer implements IRebalancer {
361
475
  );
362
476
  results.push({
363
477
  route: failedTransaction.route,
478
+ intentId: failedTransaction.route.intentId,
364
479
  success: false,
365
480
  error: `Gas estimation failed: ${String(result.reason)}`,
481
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for failures
366
482
  });
367
483
  }
368
484
  });
@@ -411,8 +527,10 @@ export class Rebalancer implements IRebalancer {
411
527
  } else {
412
528
  results.push({
413
529
  route: txResult.transaction.route,
530
+ intentId: txResult.transaction.route.intentId,
414
531
  success: false,
415
532
  error: `Transaction send failed: ${txResult.error}`,
533
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for failures
416
534
  });
417
535
  this.metrics?.recordActionAttempt(
418
536
  txResult.transaction.route,
@@ -533,7 +651,7 @@ export class Rebalancer implements IRebalancer {
533
651
  private buildResult(
534
652
  transaction: PreparedTransaction,
535
653
  receipt: providers.TransactionReceipt,
536
- ): RebalanceExecutionResult {
654
+ ): InternalExecutionResult {
537
655
  const { origin, destination } = transaction.route;
538
656
  const dispatchedMessages = HyperlaneCore.getDispatchedMessages(receipt);
539
657
 
@@ -544,14 +662,17 @@ export class Rebalancer implements IRebalancer {
544
662
  );
545
663
  return {
546
664
  route: transaction.route,
665
+ intentId: transaction.route.intentId,
547
666
  success: false,
548
667
  error: `Transaction confirmed but no Dispatch event found`,
668
+ messageId: '', // Required by MovableCollateralExecutionResult, empty for failures
549
669
  txHash: receipt.transactionHash,
550
670
  };
551
671
  }
552
672
 
553
673
  return {
554
674
  route: transaction.route,
675
+ intentId: transaction.route.intentId,
555
676
  success: true,
556
677
  messageId: dispatchedMessages[0].id,
557
678
  txHash: receipt.transactionHash,