@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.
- package/dist/config/RebalancerConfig.d.ts +2 -1
- package/dist/config/RebalancerConfig.d.ts.map +1 -1
- package/dist/config/RebalancerConfig.js +5 -3
- package/dist/config/RebalancerConfig.js.map +1 -1
- package/dist/config/RebalancerConfig.test.js +2 -1
- package/dist/config/RebalancerConfig.test.js.map +1 -1
- package/dist/config/types.d.ts +7 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +8 -0
- package/dist/config/types.js.map +1 -1
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +7 -0
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +31 -0
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/core/RebalancerOrchestrator.test.js +6 -1
- package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
- package/dist/core/RebalancerService.test.js +3 -1
- package/dist/core/RebalancerService.test.js.map +1 -1
- package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
- package/dist/e2e/harness/ForkIndexer.js +1 -0
- package/dist/e2e/harness/ForkIndexer.js.map +1 -1
- package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
- package/dist/e2e/harness/TestRebalancer.js +3 -2
- package/dist/e2e/harness/TestRebalancer.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +1 -0
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tracking/ActionTracker.d.ts +3 -2
- package/dist/tracking/ActionTracker.d.ts.map +1 -1
- package/dist/tracking/ActionTracker.js +46 -10
- package/dist/tracking/ActionTracker.js.map +1 -1
- package/dist/tracking/ActionTracker.test.js +273 -0
- package/dist/tracking/ActionTracker.test.js.map +1 -1
- package/dist/tracking/IActionTracker.d.ts +3 -2
- package/dist/tracking/IActionTracker.d.ts.map +1 -1
- package/dist/tracking/types.d.ts +3 -1
- package/dist/tracking/types.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.d.ts +1 -0
- package/dist/utils/ExplorerClient.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.js +3 -0
- package/dist/utils/ExplorerClient.js.map +1 -1
- package/package.json +7 -7
- package/src/config/RebalancerConfig.test.ts +2 -0
- package/src/config/RebalancerConfig.ts +9 -2
- package/src/config/types.ts +11 -0
- package/src/core/InventoryRebalancer.test.ts +37 -0
- package/src/core/InventoryRebalancer.ts +10 -0
- package/src/core/RebalancerOrchestrator.test.ts +10 -1
- package/src/core/RebalancerService.test.ts +6 -1
- package/src/e2e/harness/ForkIndexer.ts +1 -0
- package/src/e2e/harness/TestRebalancer.ts +3 -0
- package/src/factories/RebalancerContextFactory.ts +1 -0
- package/src/index.ts +2 -0
- package/src/tracking/ActionTracker.test.ts +321 -0
- package/src/tracking/ActionTracker.ts +61 -10
- package/src/tracking/IActionTracker.ts +3 -2
- package/src/tracking/types.ts +3 -1
- 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.
|
|
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/
|
|
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.
|
|
68
|
-
"@hyperlane-xyz/tsconfig": "^25.3.
|
|
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 {
|
|
38
|
-
|
|
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
|
);
|
package/src/config/types.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 = {
|
|
@@ -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);
|
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,
|