@hyperlane-xyz/rebalancer 3.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 (63) hide show
  1. package/dist/config/RebalancerConfig.d.ts +2 -1
  2. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  3. package/dist/config/RebalancerConfig.js +5 -3
  4. package/dist/config/RebalancerConfig.js.map +1 -1
  5. package/dist/config/RebalancerConfig.test.js +2 -1
  6. package/dist/config/RebalancerConfig.test.js.map +1 -1
  7. package/dist/config/types.d.ts +7 -0
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/config/types.js +8 -0
  10. package/dist/config/types.js.map +1 -1
  11. package/dist/core/InventoryRebalancer.d.ts.map +1 -1
  12. package/dist/core/InventoryRebalancer.js +7 -0
  13. package/dist/core/InventoryRebalancer.js.map +1 -1
  14. package/dist/core/InventoryRebalancer.test.js +31 -0
  15. package/dist/core/InventoryRebalancer.test.js.map +1 -1
  16. package/dist/core/RebalancerOrchestrator.test.js +6 -1
  17. package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
  18. package/dist/core/RebalancerService.test.js +3 -1
  19. package/dist/core/RebalancerService.test.js.map +1 -1
  20. package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
  21. package/dist/e2e/harness/ForkIndexer.js +1 -0
  22. package/dist/e2e/harness/ForkIndexer.js.map +1 -1
  23. package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
  24. package/dist/e2e/harness/TestRebalancer.js +3 -2
  25. package/dist/e2e/harness/TestRebalancer.js.map +1 -1
  26. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  27. package/dist/factories/RebalancerContextFactory.js +1 -0
  28. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/tracking/ActionTracker.d.ts +3 -2
  34. package/dist/tracking/ActionTracker.d.ts.map +1 -1
  35. package/dist/tracking/ActionTracker.js +46 -10
  36. package/dist/tracking/ActionTracker.js.map +1 -1
  37. package/dist/tracking/ActionTracker.test.js +273 -0
  38. package/dist/tracking/ActionTracker.test.js.map +1 -1
  39. package/dist/tracking/IActionTracker.d.ts +3 -2
  40. package/dist/tracking/IActionTracker.d.ts.map +1 -1
  41. package/dist/tracking/types.d.ts +3 -1
  42. package/dist/tracking/types.d.ts.map +1 -1
  43. package/dist/utils/ExplorerClient.d.ts +1 -0
  44. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  45. package/dist/utils/ExplorerClient.js +3 -0
  46. package/dist/utils/ExplorerClient.js.map +1 -1
  47. package/package.json +7 -7
  48. package/src/config/RebalancerConfig.test.ts +2 -0
  49. package/src/config/RebalancerConfig.ts +9 -2
  50. package/src/config/types.ts +11 -0
  51. package/src/core/InventoryRebalancer.test.ts +37 -0
  52. package/src/core/InventoryRebalancer.ts +10 -0
  53. package/src/core/RebalancerOrchestrator.test.ts +10 -1
  54. package/src/core/RebalancerService.test.ts +6 -1
  55. package/src/e2e/harness/ForkIndexer.ts +1 -0
  56. package/src/e2e/harness/TestRebalancer.ts +3 -0
  57. package/src/factories/RebalancerContextFactory.ts +1 -0
  58. package/src/index.ts +2 -0
  59. package/src/tracking/ActionTracker.test.ts +321 -0
  60. package/src/tracking/ActionTracker.ts +61 -10
  61. package/src/tracking/IActionTracker.ts +3 -2
  62. package/src/tracking/types.ts +3 -1
  63. package/src/utils/ExplorerClient.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperlane-xyz/rebalancer",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Hyperlane Warp Route Collateral Rebalancer Service",
5
5
  "keywords": [
6
6
  "blockchain",
@@ -40,11 +40,11 @@
40
40
  "yaml": "2.4.5",
41
41
  "zod": "^3.21.2",
42
42
  "zod-validation-error": "^3.3.0",
43
- "@hyperlane-xyz/metrics": "0.1.6",
44
- "@hyperlane-xyz/provider-sdk": "1.3.4",
45
- "@hyperlane-xyz/sdk": "25.3.0",
46
43
  "@hyperlane-xyz/core": "10.1.5",
47
- "@hyperlane-xyz/utils": "25.3.0"
44
+ "@hyperlane-xyz/metrics": "0.1.7",
45
+ "@hyperlane-xyz/provider-sdk": "1.3.5",
46
+ "@hyperlane-xyz/sdk": "25.3.1",
47
+ "@hyperlane-xyz/utils": "25.3.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@ethersproject/constants": "*",
@@ -64,8 +64,8 @@
64
64
  "testcontainers": "11.7.0",
65
65
  "tsx": "^4.19.1",
66
66
  "typescript": "5.8.3",
67
- "@hyperlane-xyz/relayer": "1.1.1",
68
- "@hyperlane-xyz/tsconfig": "^25.3.0"
67
+ "@hyperlane-xyz/relayer": "1.1.2",
68
+ "@hyperlane-xyz/tsconfig": "^25.3.1"
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=18"
@@ -9,6 +9,7 @@ import { writeYamlOrJson } from '@hyperlane-xyz/utils/fs';
9
9
 
10
10
  import { RebalancerConfig } from './RebalancerConfig.js';
11
11
  import {
12
+ DEFAULT_INTENT_TTL_MS,
12
13
  ExecutionType,
13
14
  ExternalBridgeType,
14
15
  type RebalancerConfigFileInput,
@@ -100,6 +101,7 @@ describe('RebalancerConfig', () => {
100
101
  },
101
102
  },
102
103
  ],
104
+ intentTTL: DEFAULT_INTENT_TTL_MS,
103
105
  inventorySigner: undefined,
104
106
  externalBridges: undefined,
105
107
  });
@@ -17,6 +17,7 @@ export class RebalancerConfig {
17
17
  constructor(
18
18
  public readonly warpRouteId: string,
19
19
  public readonly strategyConfig: StrategyConfig[],
20
+ public readonly intentTTL: number,
20
21
  public readonly inventorySigner?: string,
21
22
  public readonly externalBridges?: ExternalBridgesConfig,
22
23
  ) {}
@@ -34,8 +35,13 @@ export class RebalancerConfig {
34
35
  throw new Error(fromZodError(validationResult.error).message);
35
36
  }
36
37
 
37
- const { warpRouteId, strategy, inventorySigner, externalBridges } =
38
- validationResult.data;
38
+ const {
39
+ warpRouteId,
40
+ strategy,
41
+ intentTTL,
42
+ inventorySigner,
43
+ externalBridges,
44
+ } = validationResult.data;
39
45
 
40
46
  const chainNames = getStrategyChainNames(strategy);
41
47
  if (chainNames.length === 0) {
@@ -45,6 +51,7 @@ export class RebalancerConfig {
45
51
  return new RebalancerConfig(
46
52
  warpRouteId,
47
53
  strategy,
54
+ intentTTL,
48
55
  inventorySigner,
49
56
  externalBridges,
50
57
  );
@@ -115,6 +115,9 @@ export const RebalancerStrategySchema = z
115
115
  .union([StrategyConfigSchema, z.array(StrategyConfigSchema).min(1)])
116
116
  .transform((val) => (Array.isArray(val) ? val : [val]));
117
117
 
118
+ export const DEFAULT_INTENT_TTL_S = 604800; // 7 days
119
+ export const DEFAULT_INTENT_TTL_MS = DEFAULT_INTENT_TTL_S * 1_000;
120
+
118
121
  export const LiFiBridgeConfigSchema = z.object({
119
122
  integrator: z.string(),
120
123
  defaultSlippage: z.number().optional(),
@@ -133,6 +136,14 @@ export const RebalancerConfigSchema = z
133
136
  .regex(/0x[a-fA-F0-9]{40}/)
134
137
  .optional(),
135
138
  externalBridges: ExternalBridgesConfigSchema.optional(),
139
+ intentTTL: z
140
+ .number()
141
+ .positive()
142
+ .default(DEFAULT_INTENT_TTL_S)
143
+ .describe(
144
+ 'Max age in seconds before in-progress intent is expired. Default 7 days.',
145
+ )
146
+ .transform((val) => val * 1_000),
136
147
  })
137
148
  .superRefine((config, ctx) => {
138
149
  // CollateralDeficitStrategy must be first in composite if it is used
@@ -418,6 +418,7 @@ describe('InventoryRebalancer E2E', () => {
418
418
  intent: existingIntent,
419
419
  completedAmount: 3000000000n,
420
420
  remaining: 7000000000n, // 7k remaining
421
+ hasInflightDeposit: false,
421
422
  },
422
423
  ]);
423
424
 
@@ -441,6 +442,41 @@ describe('InventoryRebalancer E2E', () => {
441
442
  expect(actionTracker.createRebalanceIntent.called).to.be.false;
442
443
  });
443
444
 
445
+ it('returns empty when active intent has in-flight deposit (prevents oscillation)', async () => {
446
+ const existingIntent = createTestIntent({
447
+ id: 'inflight-deposit-intent',
448
+ status: 'in_progress',
449
+ amount: 10000000000n,
450
+ });
451
+
452
+ // Configure mock: active intent WITH in-flight deposit
453
+ actionTracker.getPartiallyFulfilledInventoryIntents.resolves([
454
+ {
455
+ intent: existingIntent,
456
+ completedAmount: 3000000000n,
457
+ remaining: 7000000000n,
458
+ hasInflightDeposit: true,
459
+ },
460
+ ]);
461
+
462
+ // New routes that WOULD create a new intent or continue
463
+ const newRoute = createTestRoute({ amount: 5000000000n });
464
+
465
+ inventoryRebalancer.setInventoryBalances({
466
+ [SOLANA_CHAIN]: 10000000000n,
467
+ [ARBITRUM_CHAIN]: 0n,
468
+ });
469
+
470
+ const results = await inventoryRebalancer.rebalance([newRoute]);
471
+
472
+ // Must return empty — wait for in-flight deposit to complete
473
+ expect(results).to.have.lengthOf(0);
474
+ // Must NOT create a new intent
475
+ expect(actionTracker.createRebalanceIntent.called).to.be.false;
476
+ // Must NOT call createRebalanceAction (proves continueIntent was never reached)
477
+ expect(actionTracker.createRebalanceAction.called).to.be.false;
478
+ });
479
+
444
480
  it('returns empty results when no routes provided and no active intent', async () => {
445
481
  const results = await inventoryRebalancer.rebalance([]);
446
482
 
@@ -463,6 +499,7 @@ describe('InventoryRebalancer E2E', () => {
463
499
  intent: existingIntent,
464
500
  completedAmount: 0n,
465
501
  remaining: 10000000000n,
502
+ hasInflightDeposit: false,
466
503
  },
467
504
  ]);
468
505
 
@@ -282,6 +282,16 @@ export class InventoryRebalancer implements IInventoryRebalancer {
282
282
  const activeIntent = await this.getActiveInventoryIntent();
283
283
 
284
284
  if (activeIntent) {
285
+ if (activeIntent.hasInflightDeposit) {
286
+ this.logger.info(
287
+ {
288
+ intentId: activeIntent.intent.id,
289
+ remaining: activeIntent.remaining.toString(),
290
+ },
291
+ 'Active intent has in-flight deposit, waiting for delivery before continuing',
292
+ );
293
+ return [];
294
+ }
285
295
  // Continue existing intent, ignore new routes
286
296
  this.logger.info(
287
297
  {
@@ -4,7 +4,11 @@ import { pino } from 'pino';
4
4
  import Sinon from 'sinon';
5
5
 
6
6
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
7
- import { ExecutionType, RebalancerStrategyOptions } from '../config/types.js';
7
+ import {
8
+ DEFAULT_INTENT_TTL_MS,
9
+ ExecutionType,
10
+ RebalancerStrategyOptions,
11
+ } from '../config/types.js';
8
12
  import type { IExternalBridge } from '../interfaces/IExternalBridge.js';
9
13
  import { MonitorEventType } from '../interfaces/IMonitor.js';
10
14
  import type { IRebalancer } from '../interfaces/IRebalancer.js';
@@ -43,6 +47,7 @@ function createMockRebalancerConfig(): RebalancerConfig {
43
47
  },
44
48
  },
45
49
  ],
50
+ intentTTL: DEFAULT_INTENT_TTL_MS,
46
51
  } as RebalancerConfig;
47
52
  }
48
53
 
@@ -372,6 +377,7 @@ describe('RebalancerOrchestrator', () => {
372
377
  },
373
378
  },
374
379
  ],
380
+ intentTTL: DEFAULT_INTENT_TTL_MS,
375
381
  } as RebalancerConfig;
376
382
 
377
383
  const strategy = createMockStrategy();
@@ -446,6 +452,7 @@ describe('RebalancerOrchestrator', () => {
446
452
  },
447
453
  },
448
454
  ],
455
+ intentTTL: DEFAULT_INTENT_TTL_MS,
449
456
  } as RebalancerConfig;
450
457
 
451
458
  const strategy = createMockStrategy();
@@ -541,6 +548,7 @@ describe('RebalancerOrchestrator', () => {
541
548
  },
542
549
  },
543
550
  ],
551
+ intentTTL: DEFAULT_INTENT_TTL_MS,
544
552
  } as RebalancerConfig;
545
553
 
546
554
  const strategy = createMockStrategy();
@@ -591,6 +599,7 @@ describe('RebalancerOrchestrator', () => {
591
599
  },
592
600
  },
593
601
  ],
602
+ intentTTL: DEFAULT_INTENT_TTL_MS,
594
603
  } as RebalancerConfig;
595
604
 
596
605
  const strategy = createMockStrategy();
@@ -6,7 +6,10 @@ import Sinon from 'sinon';
6
6
  import type { MultiProvider, Token, WarpCore } from '@hyperlane-xyz/sdk';
7
7
 
8
8
  import type { RebalancerConfig } from '../config/RebalancerConfig.js';
9
- import { RebalancerStrategyOptions } from '../config/types.js';
9
+ import {
10
+ DEFAULT_INTENT_TTL_MS,
11
+ RebalancerStrategyOptions,
12
+ } from '../config/types.js';
10
13
  import { RebalancerContextFactory } from '../factories/RebalancerContextFactory.js';
11
14
  import type { ExternalBridgeRegistry } from '../interfaces/IExternalBridge.js';
12
15
  import { MonitorEventType } from '../interfaces/IMonitor.js';
@@ -47,6 +50,7 @@ function createMockRebalancerConfig(): RebalancerConfig {
47
50
  },
48
51
  },
49
52
  ],
53
+ intentTTL: DEFAULT_INTENT_TTL_MS,
50
54
  } as RebalancerConfig;
51
55
  }
52
56
 
@@ -476,6 +480,7 @@ describe('RebalancerService', () => {
476
480
  },
477
481
  },
478
482
  ],
483
+ intentTTL: DEFAULT_INTENT_TTL_MS,
479
484
  } as RebalancerConfig;
480
485
 
481
486
  const config: RebalancerServiceConfig = {
@@ -120,6 +120,7 @@ export class ForkIndexer {
120
120
  origin_tx_recipient: event.args.sender,
121
121
  is_delivered: false,
122
122
  message_body: parsed.body,
123
+ send_occurred_at: null,
123
124
  };
124
125
 
125
126
  if (
@@ -10,6 +10,7 @@ import { addressToBytes32 } from '@hyperlane-xyz/utils';
10
10
 
11
11
  import { RebalancerConfig } from '../../config/RebalancerConfig.js';
12
12
  import {
13
+ DEFAULT_INTENT_TTL_MS,
13
14
  type StrategyConfig,
14
15
  getStrategyChainNames,
15
16
  } from '../../config/types.js';
@@ -203,6 +204,7 @@ export class TestRebalancerBuilder {
203
204
  const rebalancerConfig = new RebalancerConfig(
204
205
  MONITORED_ROUTE_ID,
205
206
  this.strategyConfig,
207
+ DEFAULT_INTENT_TTL_MS,
206
208
  );
207
209
 
208
210
  const registry = this.deploymentManager.getRegistry();
@@ -325,6 +327,7 @@ export class TestRebalancerBuilder {
325
327
  origin_tx_recipient: deployedAddresses.monitoredRoute[params.from],
326
328
  is_delivered: false,
327
329
  message_body: encodeWarpRouteMessageBody(warpRecipient, params.amount),
330
+ send_occurred_at: null,
328
331
  };
329
332
 
330
333
  userTransfers.push(mockTransfer);
@@ -351,6 +351,7 @@ export class RebalancerContextFactory {
351
351
  bridges,
352
352
  rebalancerAddress,
353
353
  inventorySignerAddress: this.config.inventorySigner,
354
+ intentTTL: this.config.intentTTL,
354
355
  };
355
356
 
356
357
  // 6. Create ActionTracker
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export { Rebalancer } from './core/Rebalancer.js';
20
20
  // Configuration
21
21
  export { RebalancerConfig } from './config/RebalancerConfig.js';
22
22
  export {
23
+ DEFAULT_INTENT_TTL_MS,
24
+ DEFAULT_INTENT_TTL_S,
23
25
  getStrategyChainConfig,
24
26
  getStrategyChainNames,
25
27
  RebalancerBaseChainConfigSchema,