@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
@@ -5,6 +5,7 @@ import Sinon from 'sinon';
5
5
 
6
6
  import { EthJsonRpcBlockParameterTag } from '@hyperlane-xyz/sdk';
7
7
 
8
+ import { DEFAULT_INTENT_TTL_MS } from '../config/types.js';
8
9
  import type { ExplorerMessage } from '../utils/ExplorerClient.js';
9
10
 
10
11
  import { ActionTracker, type ActionTrackerConfig } from './ActionTracker.js';
@@ -71,6 +72,7 @@ describe('ActionTracker', () => {
71
72
  },
72
73
  bridges: ['0xbridge1', '0xbridge2'],
73
74
  rebalancerAddress: '0xrebalancer',
75
+ intentTTL: DEFAULT_INTENT_TTL_MS,
74
76
  };
75
77
 
76
78
  tracker = new ActionTracker(
@@ -99,6 +101,7 @@ describe('ActionTracker', () => {
99
101
  is_delivered: false,
100
102
  message_body:
101
103
  '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
104
+ send_occurred_at: null,
102
105
  },
103
106
  ];
104
107
 
@@ -128,6 +131,102 @@ describe('ActionTracker', () => {
128
131
  expect(actions[0].messageId).to.equal('0xmsg1');
129
132
  });
130
133
 
134
+ it('should use send_occurred_at for createdAt when available', async () => {
135
+ const inflightMessages: ExplorerMessage[] = [
136
+ {
137
+ msg_id: '0xmsg1',
138
+ origin_domain_id: 1,
139
+ destination_domain_id: 2,
140
+ sender: '0xrouter1',
141
+ recipient: '0xrouter2',
142
+ origin_tx_hash: '0xtx1',
143
+ origin_tx_sender: '0xrebalancer',
144
+ origin_tx_recipient: '0xrouter1',
145
+ is_delivered: false,
146
+ message_body:
147
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
148
+ send_occurred_at: '2024-01-15T12:30:45',
149
+ },
150
+ ];
151
+
152
+ explorerClient.getInflightRebalanceActions.resolves(inflightMessages);
153
+ explorerClient.getInflightUserTransfers.resolves([]);
154
+ mailboxStub.isDelivered.resolves(false);
155
+
156
+ await tracker.initialize();
157
+
158
+ const intents = await rebalanceIntentStore.getAll();
159
+ expect(intents).to.have.lengthOf(1);
160
+ // Hasura timestamps are UTC without 'Z'; recoverAction appends 'Z' before parsing
161
+ const expectedMs = new Date('2024-01-15T12:30:45Z').getTime();
162
+ expect(intents[0].createdAt).to.equal(expectedMs);
163
+
164
+ const actions = await rebalanceActionStore.getAll();
165
+ expect(actions[0].createdAt).to.equal(expectedMs);
166
+ });
167
+
168
+ it('should fall back to Date.now() when send_occurred_at is null', async () => {
169
+ const before = Date.now();
170
+ const inflightMessages: ExplorerMessage[] = [
171
+ {
172
+ msg_id: '0xmsg1',
173
+ origin_domain_id: 1,
174
+ destination_domain_id: 2,
175
+ sender: '0xrouter1',
176
+ recipient: '0xrouter2',
177
+ origin_tx_hash: '0xtx1',
178
+ origin_tx_sender: '0xrebalancer',
179
+ origin_tx_recipient: '0xrouter1',
180
+ is_delivered: false,
181
+ message_body:
182
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
183
+ send_occurred_at: null,
184
+ },
185
+ ];
186
+
187
+ explorerClient.getInflightRebalanceActions.resolves(inflightMessages);
188
+ explorerClient.getInflightUserTransfers.resolves([]);
189
+ mailboxStub.isDelivered.resolves(false);
190
+
191
+ await tracker.initialize();
192
+ const after = Date.now();
193
+
194
+ const intents = await rebalanceIntentStore.getAll();
195
+ expect(intents[0].createdAt).to.be.at.least(before);
196
+ expect(intents[0].createdAt).to.be.at.most(after);
197
+ });
198
+
199
+ it('should skip action with invalid send_occurred_at timestamp', async () => {
200
+ const inflightMessages: ExplorerMessage[] = [
201
+ {
202
+ msg_id: '0xmsg1',
203
+ origin_domain_id: 1,
204
+ destination_domain_id: 2,
205
+ sender: '0xrouter1',
206
+ recipient: '0xrouter2',
207
+ origin_tx_hash: '0xtx1',
208
+ origin_tx_sender: '0xrebalancer',
209
+ origin_tx_recipient: '0xrouter1',
210
+ is_delivered: false,
211
+ message_body:
212
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
213
+ send_occurred_at: 'garbage',
214
+ },
215
+ ];
216
+
217
+ explorerClient.getInflightRebalanceActions.resolves(inflightMessages);
218
+ explorerClient.getInflightUserTransfers.resolves([]);
219
+ mailboxStub.isDelivered.resolves(false);
220
+
221
+ await tracker.initialize();
222
+
223
+ // Invalid timestamp is caught by recoverAction's catch block, so the action is skipped
224
+ const intents = await rebalanceIntentStore.getAll();
225
+ expect(intents).to.have.lengthOf(0);
226
+ const actions = await rebalanceActionStore.getAll();
227
+ expect(actions).to.have.lengthOf(0);
228
+ });
229
+
131
230
  it('should skip creating action if it already exists', async () => {
132
231
  const inflightMessages: ExplorerMessage[] = [
133
232
  {
@@ -142,12 +241,14 @@ describe('ActionTracker', () => {
142
241
  is_delivered: false,
143
242
  message_body:
144
243
  '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
244
+ send_occurred_at: null,
145
245
  },
146
246
  ];
147
247
 
148
248
  // Pre-create action
149
249
  await rebalanceActionStore.save({
150
250
  id: '0xmsg1',
251
+ type: 'rebalance_message',
151
252
  status: 'in_progress',
152
253
  intentId: 'existing-intent',
153
254
  messageId: '0xmsg1',
@@ -188,6 +289,7 @@ describe('ActionTracker', () => {
188
289
  is_delivered: false,
189
290
  message_body:
190
291
  '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
292
+ send_occurred_at: null,
191
293
  },
192
294
  ];
193
295
 
@@ -231,6 +333,7 @@ describe('ActionTracker', () => {
231
333
  is_delivered: false,
232
334
  message_body:
233
335
  '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064',
336
+ send_occurred_at: null,
234
337
  },
235
338
  ];
236
339
 
@@ -269,18 +372,32 @@ describe('ActionTracker', () => {
269
372
 
270
373
  describe('syncRebalanceIntents', () => {
271
374
  it('should mark intents as complete when fully fulfilled', async () => {
375
+ // Intent derives completion from action states, so we need a complete action
272
376
  const intent: RebalanceIntent = {
273
377
  id: 'intent-1',
274
378
  status: 'in_progress',
275
379
  origin: 1,
276
380
  destination: 2,
277
381
  amount: 100n,
278
- fulfilledAmount: 100n,
382
+ createdAt: Date.now(),
383
+ updatedAt: Date.now(),
384
+ };
385
+
386
+ const action: RebalanceAction = {
387
+ id: 'action-1',
388
+ type: 'rebalance_message',
389
+ status: 'complete',
390
+ intentId: 'intent-1',
391
+ messageId: '0xmsg1',
392
+ origin: 1,
393
+ destination: 2,
394
+ amount: 100n,
279
395
  createdAt: Date.now(),
280
396
  updatedAt: Date.now(),
281
397
  };
282
398
 
283
399
  await rebalanceIntentStore.save(intent);
400
+ await rebalanceActionStore.save(action);
284
401
 
285
402
  await tracker.syncRebalanceIntents();
286
403
 
@@ -289,17 +406,86 @@ describe('ActionTracker', () => {
289
406
  });
290
407
 
291
408
  it('should not mark intents as complete if not fully fulfilled', async () => {
409
+ // Intent with only partial completion via actions
410
+ const intent: RebalanceIntent = {
411
+ id: 'intent-1',
412
+ status: 'in_progress',
413
+ origin: 1,
414
+ destination: 2,
415
+ amount: 100n,
416
+ createdAt: Date.now(),
417
+ updatedAt: Date.now(),
418
+ };
419
+
420
+ const action: RebalanceAction = {
421
+ id: 'action-1',
422
+ type: 'rebalance_message',
423
+ status: 'complete',
424
+ intentId: 'intent-1',
425
+ messageId: '0xmsg1',
426
+ origin: 1,
427
+ destination: 2,
428
+ amount: 50n, // Only partial
429
+ createdAt: Date.now(),
430
+ updatedAt: Date.now(),
431
+ };
432
+
433
+ await rebalanceIntentStore.save(intent);
434
+ await rebalanceActionStore.save(action);
435
+
436
+ await tracker.syncRebalanceIntents();
437
+
438
+ const updated = await rebalanceIntentStore.get('intent-1');
439
+ expect(updated?.status).to.equal('in_progress');
440
+ });
441
+
442
+ it('should mark unfulfilled intents as failed when TTL exceeded', async () => {
292
443
  const intent: RebalanceIntent = {
293
444
  id: 'intent-1',
294
445
  status: 'in_progress',
295
446
  origin: 1,
296
447
  destination: 2,
297
448
  amount: 100n,
298
- fulfilledAmount: 50n,
449
+ createdAt: Date.now() - DEFAULT_INTENT_TTL_MS - 1,
450
+ updatedAt: Date.now(),
451
+ };
452
+
453
+ const action: RebalanceAction = {
454
+ id: 'action-1',
455
+ type: 'rebalance_message',
456
+ status: 'in_progress',
457
+ intentId: 'intent-1',
458
+ messageId: '0xmsg1',
459
+ origin: 1,
460
+ destination: 2,
461
+ amount: 50n,
299
462
  createdAt: Date.now(),
300
463
  updatedAt: Date.now(),
301
464
  };
302
465
 
466
+ await rebalanceIntentStore.save(intent);
467
+ await rebalanceActionStore.save(action);
468
+
469
+ await tracker.syncRebalanceIntents();
470
+
471
+ const updatedIntent = await rebalanceIntentStore.get('intent-1');
472
+ expect(updatedIntent?.status).to.equal('failed');
473
+
474
+ const updatedAction = await rebalanceActionStore.get('action-1');
475
+ expect(updatedAction?.status).to.equal('failed');
476
+ });
477
+
478
+ it('should not expire intents within TTL', async () => {
479
+ const intent: RebalanceIntent = {
480
+ id: 'intent-1',
481
+ status: 'in_progress',
482
+ origin: 1,
483
+ destination: 2,
484
+ amount: 100n,
485
+ createdAt: Date.now() - DEFAULT_INTENT_TTL_MS + 60_000,
486
+ updatedAt: Date.now(),
487
+ };
488
+
303
489
  await rebalanceIntentStore.save(intent);
304
490
 
305
491
  await tracker.syncRebalanceIntents();
@@ -317,13 +503,13 @@ describe('ActionTracker', () => {
317
503
  origin: 1,
318
504
  destination: 2,
319
505
  amount: 100n,
320
- fulfilledAmount: 0n,
321
506
  createdAt: Date.now(),
322
507
  updatedAt: Date.now(),
323
508
  };
324
509
 
325
510
  const action: RebalanceAction = {
326
511
  id: 'action-1',
512
+ type: 'rebalance_message',
327
513
  status: 'in_progress',
328
514
  intentId: 'intent-1',
329
515
  messageId: '0xmsg1',
@@ -345,15 +531,15 @@ describe('ActionTracker', () => {
345
531
  const updatedAction = await rebalanceActionStore.get('action-1');
346
532
  expect(updatedAction?.status).to.equal('complete');
347
533
 
348
- // Intent should be updated and complete
534
+ // Intent should be complete (derived from completed action amounts)
349
535
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
350
- expect(updatedIntent?.fulfilledAmount).to.equal(100n);
351
536
  expect(updatedIntent?.status).to.equal('complete');
352
537
  });
353
538
 
354
539
  it('should not mark actions as complete if not delivered', async () => {
355
540
  const action: RebalanceAction = {
356
541
  id: 'action-1',
542
+ type: 'rebalance_message',
357
543
  status: 'in_progress',
358
544
  intentId: 'intent-1',
359
545
  messageId: '0xmsg1',
@@ -417,7 +603,6 @@ describe('ActionTracker', () => {
417
603
  origin: 1,
418
604
  destination: 2,
419
605
  amount: 100n,
420
- fulfilledAmount: 0n,
421
606
  createdAt: Date.now(),
422
607
  updatedAt: Date.now(),
423
608
  });
@@ -428,7 +613,6 @@ describe('ActionTracker', () => {
428
613
  origin: 2,
429
614
  destination: 3,
430
615
  amount: 200n,
431
- fulfilledAmount: 50n,
432
616
  createdAt: Date.now(),
433
617
  updatedAt: Date.now(),
434
618
  });
@@ -439,7 +623,6 @@ describe('ActionTracker', () => {
439
623
  origin: 3,
440
624
  destination: 1,
441
625
  amount: 300n,
442
- fulfilledAmount: 300n,
443
626
  createdAt: Date.now(),
444
627
  updatedAt: Date.now(),
445
628
  });
@@ -452,6 +635,249 @@ describe('ActionTracker', () => {
452
635
  });
453
636
  });
454
637
 
638
+ describe('getPartiallyFulfilledInventoryIntents', () => {
639
+ it('returns not_started inventory intents', async () => {
640
+ // Create a not_started inventory intent (simulates failed execution before any action created)
641
+ await rebalanceIntentStore.save({
642
+ id: 'stuck-intent',
643
+ status: 'not_started',
644
+ origin: 1,
645
+ destination: 2,
646
+ amount: 1000000000000000000n, // 1 ETH
647
+ executionMethod: 'inventory',
648
+ createdAt: Date.now(),
649
+ updatedAt: Date.now(),
650
+ });
651
+
652
+ // Should be returned even though status is 'not_started'
653
+ const partialIntents =
654
+ await tracker.getPartiallyFulfilledInventoryIntents();
655
+
656
+ expect(partialIntents).to.have.lengthOf(1);
657
+ expect(partialIntents[0].intent.id).to.equal('stuck-intent');
658
+ expect(partialIntents[0].completedAmount).to.equal(0n);
659
+ expect(partialIntents[0].remaining).to.equal(1000000000000000000n);
660
+ });
661
+
662
+ it('returns in_progress inventory intents with partial completion', async () => {
663
+ // Create an in_progress inventory intent with a completed action
664
+ await rebalanceIntentStore.save({
665
+ id: 'partial-intent',
666
+ status: 'in_progress',
667
+ origin: 1,
668
+ destination: 2,
669
+ amount: 1000000000000000000n, // 1 ETH
670
+ executionMethod: 'inventory',
671
+ createdAt: Date.now(),
672
+ updatedAt: Date.now(),
673
+ });
674
+
675
+ // Create a completed inventory_deposit action for partial amount
676
+ await rebalanceActionStore.save({
677
+ id: 'action-1',
678
+ type: 'inventory_deposit',
679
+ status: 'complete',
680
+ intentId: 'partial-intent',
681
+ origin: 1,
682
+ destination: 2,
683
+ amount: 400000000000000000n, // 0.4 ETH completed
684
+ createdAt: Date.now(),
685
+ updatedAt: Date.now(),
686
+ });
687
+
688
+ const partialIntents =
689
+ await tracker.getPartiallyFulfilledInventoryIntents();
690
+
691
+ expect(partialIntents).to.have.lengthOf(1);
692
+ expect(partialIntents[0].intent.id).to.equal('partial-intent');
693
+ expect(partialIntents[0].completedAmount).to.equal(400000000000000000n);
694
+ expect(partialIntents[0].remaining).to.equal(600000000000000000n); // 0.6 ETH remaining
695
+ });
696
+
697
+ it('does not return non-inventory intents', async () => {
698
+ // Create a not_started intent without executionMethod: 'inventory'
699
+ await rebalanceIntentStore.save({
700
+ id: 'non-inventory-intent',
701
+ status: 'not_started',
702
+ origin: 1,
703
+ destination: 2,
704
+ amount: 1000000000000000000n,
705
+ // executionMethod is undefined - not an inventory intent
706
+ createdAt: Date.now(),
707
+ updatedAt: Date.now(),
708
+ });
709
+
710
+ const partialIntents =
711
+ await tracker.getPartiallyFulfilledInventoryIntents();
712
+
713
+ expect(partialIntents).to.have.lengthOf(0);
714
+ });
715
+
716
+ it('returns intent with in-flight deposit and sets hasInflightDeposit flag', async () => {
717
+ // Setup: in_progress inventory intent (amount: 1 ETH = 1_000_000_000_000_000_000n)
718
+ await rebalanceIntentStore.save({
719
+ id: 'intent-with-inflight',
720
+ status: 'in_progress',
721
+ origin: 1,
722
+ destination: 2,
723
+ amount: 1000000000000000000n, // 1 ETH
724
+ executionMethod: 'inventory',
725
+ createdAt: Date.now(),
726
+ updatedAt: Date.now(),
727
+ });
728
+
729
+ // Complete inventory_deposit action (amount: 400_000_000_000_000_000n = 0.4 ETH)
730
+ await rebalanceActionStore.save({
731
+ id: 'action-complete',
732
+ type: 'inventory_deposit',
733
+ status: 'complete',
734
+ intentId: 'intent-with-inflight',
735
+ origin: 1,
736
+ destination: 2,
737
+ amount: 400000000000000000n, // 0.4 ETH completed
738
+ createdAt: Date.now(),
739
+ updatedAt: Date.now(),
740
+ });
741
+
742
+ // In_progress inventory_deposit action (amount: 300_000_000_000_000_000n = 0.3 ETH)
743
+ await rebalanceActionStore.save({
744
+ id: 'action-inflight',
745
+ type: 'inventory_deposit',
746
+ status: 'in_progress',
747
+ intentId: 'intent-with-inflight',
748
+ origin: 1,
749
+ destination: 2,
750
+ amount: 300000000000000000n, // 0.3 ETH in-flight
751
+ createdAt: Date.now(),
752
+ updatedAt: Date.now(),
753
+ });
754
+
755
+ const partialIntents =
756
+ await tracker.getPartiallyFulfilledInventoryIntents();
757
+
758
+ expect(partialIntents).to.have.lengthOf(1);
759
+ expect(partialIntents[0].hasInflightDeposit).to.be.true;
760
+ expect(partialIntents[0].completedAmount).to.equal(400000000000000000n);
761
+ expect(partialIntents[0].remaining).to.equal(300000000000000000n); // 1.0 - 0.4 - 0.3
762
+ });
763
+
764
+ it('returns intent without in-flight deposit with hasInflightDeposit false', async () => {
765
+ // Setup: in_progress inventory intent (amount: 1 ETH)
766
+ await rebalanceIntentStore.save({
767
+ id: 'intent-no-inflight',
768
+ status: 'in_progress',
769
+ origin: 1,
770
+ destination: 2,
771
+ amount: 1000000000000000000n, // 1 ETH
772
+ executionMethod: 'inventory',
773
+ createdAt: Date.now(),
774
+ updatedAt: Date.now(),
775
+ });
776
+
777
+ // Complete inventory_deposit action (amount: 0.4 ETH)
778
+ await rebalanceActionStore.save({
779
+ id: 'action-complete-only',
780
+ type: 'inventory_deposit',
781
+ status: 'complete',
782
+ intentId: 'intent-no-inflight',
783
+ origin: 1,
784
+ destination: 2,
785
+ amount: 400000000000000000n, // 0.4 ETH completed
786
+ createdAt: Date.now(),
787
+ updatedAt: Date.now(),
788
+ });
789
+ // NO in_progress inventory_deposit actions
790
+
791
+ const partialIntents =
792
+ await tracker.getPartiallyFulfilledInventoryIntents();
793
+
794
+ expect(partialIntents).to.have.lengthOf(1);
795
+ expect(partialIntents[0].hasInflightDeposit).to.be.false;
796
+ expect(partialIntents[0].remaining).to.equal(600000000000000000n);
797
+ });
798
+
799
+ it('returns intent when remaining is 0n but has in-flight deposit', async () => {
800
+ // intent.amount = 1 ETH, completedAmount = 0.7 ETH, inflightAmount = 0.3 ETH → remaining = 0n
801
+ await rebalanceIntentStore.save({
802
+ id: 'intent-zero-remaining',
803
+ status: 'in_progress',
804
+ origin: 1,
805
+ destination: 2,
806
+ amount: 1000000000000000000n, // 1 ETH
807
+ executionMethod: 'inventory',
808
+ createdAt: Date.now(),
809
+ updatedAt: Date.now(),
810
+ });
811
+
812
+ // 0.7 ETH already completed
813
+ await rebalanceActionStore.save({
814
+ id: 'action-complete-part',
815
+ type: 'inventory_deposit',
816
+ status: 'complete',
817
+ intentId: 'intent-zero-remaining',
818
+ origin: 1,
819
+ destination: 2,
820
+ amount: 700000000000000000n, // 0.7 ETH
821
+ createdAt: Date.now(),
822
+ updatedAt: Date.now(),
823
+ });
824
+
825
+ // 0.3 ETH in-flight — exactly fills the remaining gap
826
+ await rebalanceActionStore.save({
827
+ id: 'action-inflight-rest',
828
+ type: 'inventory_deposit',
829
+ status: 'in_progress',
830
+ intentId: 'intent-zero-remaining',
831
+ origin: 1,
832
+ destination: 2,
833
+ amount: 300000000000000000n, // 0.3 ETH in-flight
834
+ createdAt: Date.now(),
835
+ updatedAt: Date.now(),
836
+ });
837
+
838
+ const partialIntents =
839
+ await tracker.getPartiallyFulfilledInventoryIntents();
840
+
841
+ expect(partialIntents).to.have.lengthOf(1);
842
+ expect(partialIntents[0].remaining).to.equal(0n);
843
+ expect(partialIntents[0].hasInflightDeposit).to.be.true;
844
+ expect(partialIntents[0].completedAmount).to.equal(700000000000000000n);
845
+ });
846
+
847
+ it('does not return fully completed intent with no in-flight deposits', async () => {
848
+ // intent.amount = 1 ETH, completedAmount = 1 ETH, inflightAmount = 0 → remaining = 0n, no inflight
849
+ await rebalanceIntentStore.save({
850
+ id: 'intent-fully-done',
851
+ status: 'in_progress',
852
+ origin: 1,
853
+ destination: 2,
854
+ amount: 1000000000000000000n, // 1 ETH
855
+ executionMethod: 'inventory',
856
+ createdAt: Date.now(),
857
+ updatedAt: Date.now(),
858
+ });
859
+
860
+ // Full amount completed
861
+ await rebalanceActionStore.save({
862
+ id: 'action-all-done',
863
+ type: 'inventory_deposit',
864
+ status: 'complete',
865
+ intentId: 'intent-fully-done',
866
+ origin: 1,
867
+ destination: 2,
868
+ amount: 1000000000000000000n, // 1 ETH — full amount
869
+ createdAt: Date.now(),
870
+ updatedAt: Date.now(),
871
+ });
872
+
873
+ const partialIntents =
874
+ await tracker.getPartiallyFulfilledInventoryIntents();
875
+
876
+ // remaining = 0n AND inflightAmount = 0n → should NOT be returned
877
+ expect(partialIntents).to.have.lengthOf(0);
878
+ });
879
+ });
880
+
455
881
  describe('createRebalanceIntent', () => {
456
882
  it('should create a new intent with status not_started', async () => {
457
883
  const result = await tracker.createRebalanceIntent({
@@ -466,7 +892,6 @@ describe('ActionTracker', () => {
466
892
  expect(result.origin).to.equal(1);
467
893
  expect(result.destination).to.equal(2);
468
894
  expect(result.amount).to.equal(100n);
469
- expect(result.fulfilledAmount).to.equal(0n);
470
895
  expect(result.priority).to.equal(1);
471
896
  expect(result.strategyType).to.equal('MinAmountStrategy');
472
897
 
@@ -483,7 +908,6 @@ describe('ActionTracker', () => {
483
908
  origin: 1,
484
909
  destination: 2,
485
910
  amount: 100n,
486
- fulfilledAmount: 0n,
487
911
  createdAt: Date.now(),
488
912
  updatedAt: Date.now(),
489
913
  };
@@ -491,6 +915,7 @@ describe('ActionTracker', () => {
491
915
  await rebalanceIntentStore.save(intent);
492
916
 
493
917
  const result = await tracker.createRebalanceAction({
918
+ type: 'rebalance_message',
494
919
  intentId: 'intent-1',
495
920
  origin: 1,
496
921
  destination: 2,
@@ -514,7 +939,6 @@ describe('ActionTracker', () => {
514
939
  origin: 1,
515
940
  destination: 2,
516
941
  amount: 100n,
517
- fulfilledAmount: 50n,
518
942
  createdAt: Date.now(),
519
943
  updatedAt: Date.now(),
520
944
  };
@@ -522,6 +946,7 @@ describe('ActionTracker', () => {
522
946
  await rebalanceIntentStore.save(intent);
523
947
 
524
948
  await tracker.createRebalanceAction({
949
+ type: 'rebalance_message',
525
950
  intentId: 'intent-1',
526
951
  origin: 1,
527
952
  destination: 2,
@@ -532,25 +957,24 @@ describe('ActionTracker', () => {
532
957
 
533
958
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
534
959
  expect(updatedIntent?.status).to.equal('in_progress');
535
- expect(updatedIntent?.fulfilledAmount).to.equal(50n); // Should not change
536
960
  });
537
961
  });
538
962
 
539
963
  describe('completeRebalanceAction', () => {
540
- it('should mark action as complete and update parent intent fulfilledAmount', async () => {
964
+ it('should mark action as complete and mark parent intent complete if fully fulfilled', async () => {
541
965
  const intent: RebalanceIntent = {
542
966
  id: 'intent-1',
543
967
  status: 'in_progress',
544
968
  origin: 1,
545
969
  destination: 2,
546
970
  amount: 100n,
547
- fulfilledAmount: 0n,
548
971
  createdAt: Date.now(),
549
972
  updatedAt: Date.now(),
550
973
  };
551
974
 
552
975
  const action: RebalanceAction = {
553
976
  id: 'action-1',
977
+ type: 'rebalance_message',
554
978
  status: 'in_progress',
555
979
  intentId: 'intent-1',
556
980
  messageId: '0xmsg1',
@@ -569,8 +993,8 @@ describe('ActionTracker', () => {
569
993
  const updatedAction = await rebalanceActionStore.get('action-1');
570
994
  expect(updatedAction?.status).to.equal('complete');
571
995
 
996
+ // Intent should be complete (derived from completed action amounts)
572
997
  const updatedIntent = await rebalanceIntentStore.get('intent-1');
573
- expect(updatedIntent?.fulfilledAmount).to.equal(100n);
574
998
  expect(updatedIntent?.status).to.equal('complete');
575
999
  });
576
1000
 
@@ -589,7 +1013,6 @@ describe('ActionTracker', () => {
589
1013
  origin: 1,
590
1014
  destination: 2,
591
1015
  amount: 100n,
592
- fulfilledAmount: 0n,
593
1016
  createdAt: Date.now(),
594
1017
  updatedAt: Date.now(),
595
1018
  };
@@ -607,6 +1030,7 @@ describe('ActionTracker', () => {
607
1030
  it('should mark action as failed', async () => {
608
1031
  const action: RebalanceAction = {
609
1032
  id: 'action-1',
1033
+ type: 'rebalance_message',
610
1034
  status: 'in_progress',
611
1035
  intentId: 'intent-1',
612
1036
  messageId: '0xmsg1',
@@ -653,7 +1077,7 @@ describe('ActionTracker', () => {
653
1077
 
654
1078
  const params = call.args[0];
655
1079
  expect(params.routersByDomain).to.deep.equal(config.routersByDomain);
656
- expect(params.excludeTxSender).to.equal(config.rebalancerAddress);
1080
+ expect(params.excludeTxSenders).to.deep.equal([config.rebalancerAddress]);
657
1081
  });
658
1082
  });
659
1083
 
@@ -692,13 +1116,13 @@ describe('ActionTracker', () => {
692
1116
  origin: 1,
693
1117
  destination: 2,
694
1118
  amount: 100n,
695
- fulfilledAmount: 0n,
696
1119
  createdAt: Date.now(),
697
1120
  updatedAt: Date.now(),
698
1121
  };
699
1122
 
700
1123
  const action: RebalanceAction = {
701
1124
  id: 'action-1',
1125
+ type: 'rebalance_message',
702
1126
  status: 'in_progress',
703
1127
  intentId: 'intent-1',
704
1128
  messageId: '0xmsg1',
@@ -806,7 +1230,6 @@ describe('ActionTracker', () => {
806
1230
  origin: 1,
807
1231
  destination: 2,
808
1232
  amount: 100n,
809
- fulfilledAmount: 0n,
810
1233
  createdAt: Date.now(),
811
1234
  updatedAt: Date.now(),
812
1235
  };
@@ -814,6 +1237,7 @@ describe('ActionTracker', () => {
814
1237
  const action: RebalanceAction = {
815
1238
  id: 'action-1',
816
1239
  status: 'in_progress',
1240
+ type: 'rebalance_message',
817
1241
  intentId: 'intent-1',
818
1242
  messageId: '0xmsg1',
819
1243
  origin: 1,