@hyperlane-xyz/rebalancer 27.2.12 → 27.2.13
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/core/InventoryRebalancer.d.ts +11 -19
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +336 -268
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +397 -23
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/core/Rebalancer.d.ts.map +1 -1
- package/dist/core/Rebalancer.js +12 -6
- package/dist/core/Rebalancer.js.map +1 -1
- package/dist/core/Rebalancer.test.js +51 -0
- package/dist/core/Rebalancer.test.js.map +1 -1
- package/dist/core/RebalancerOrchestrator.test.js +0 -1
- package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
- package/dist/core/RebalancerService.d.ts +2 -3
- package/dist/core/RebalancerService.d.ts.map +1 -1
- package/dist/core/RebalancerService.js +3 -2
- package/dist/core/RebalancerService.js.map +1 -1
- package/dist/core/RebalancerService.test.js +24 -0
- package/dist/core/RebalancerService.test.js.map +1 -1
- package/dist/e2e/harness/TestHelpers.js +1 -2
- package/dist/e2e/harness/TestHelpers.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.d.ts +4 -5
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +12 -7
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.test.js +99 -2
- package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
- package/dist/interfaces/IRebalancer.d.ts +4 -2
- package/dist/interfaces/IRebalancer.d.ts.map +1 -1
- package/dist/metrics/scripts/metrics.d.ts +1 -1
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +14 -6
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/strategy/BaseStrategy.d.ts.map +1 -1
- package/dist/strategy/BaseStrategy.js +13 -11
- package/dist/strategy/BaseStrategy.js.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.js +2 -2
- package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.d.ts +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
- package/dist/strategy/MinAmountStrategy.js +12 -8
- package/dist/strategy/MinAmountStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.test.js +189 -2
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
- package/dist/test/helpers.d.ts +11 -3
- package/dist/test/helpers.d.ts.map +1 -1
- package/dist/test/helpers.js +9 -11
- package/dist/test/helpers.js.map +1 -1
- package/dist/test/lifiMocks.d.ts.map +1 -1
- package/dist/test/lifiMocks.js +5 -2
- package/dist/test/lifiMocks.js.map +1 -1
- package/dist/tracking/ActionTracker.d.ts.map +1 -1
- package/dist/tracking/ActionTracker.js +2 -1
- package/dist/tracking/ActionTracker.js.map +1 -1
- package/dist/tracking/ActionTracker.test.js +39 -0
- package/dist/tracking/ActionTracker.test.js.map +1 -1
- package/dist/utils/balanceUtils.d.ts +7 -1
- package/dist/utils/balanceUtils.d.ts.map +1 -1
- package/dist/utils/balanceUtils.js +39 -1
- package/dist/utils/balanceUtils.js.map +1 -1
- package/dist/utils/balanceUtils.test.js +55 -1
- package/dist/utils/balanceUtils.test.js.map +1 -1
- package/dist/utils/blockTag.d.ts +3 -3
- package/dist/utils/blockTag.d.ts.map +1 -1
- package/dist/utils/blockTag.js +1 -1
- package/dist/utils/blockTag.js.map +1 -1
- package/package.json +7 -7
- package/src/core/InventoryRebalancer.test.ts +503 -38
- package/src/core/InventoryRebalancer.ts +483 -350
- package/src/core/Rebalancer.test.ts +84 -0
- package/src/core/Rebalancer.ts +22 -6
- package/src/core/RebalancerOrchestrator.test.ts +0 -1
- package/src/core/RebalancerService.test.ts +35 -0
- package/src/core/RebalancerService.ts +9 -5
- package/src/e2e/harness/TestHelpers.ts +3 -3
- package/src/factories/RebalancerContextFactory.test.ts +143 -6
- package/src/factories/RebalancerContextFactory.ts +29 -17
- package/src/interfaces/IRebalancer.ts +4 -1
- package/src/monitor/Monitor.ts +19 -6
- package/src/strategy/BaseStrategy.ts +18 -15
- package/src/strategy/CollateralDeficitStrategy.ts +4 -3
- package/src/strategy/MinAmountStrategy.test.ts +238 -2
- package/src/strategy/MinAmountStrategy.ts +29 -17
- package/src/test/helpers.ts +13 -12
- package/src/test/lifiMocks.ts +5 -2
- package/src/tracking/ActionTracker.test.ts +47 -0
- package/src/tracking/ActionTracker.ts +2 -1
- package/src/utils/balanceUtils.test.ts +87 -1
- package/src/utils/balanceUtils.ts +73 -2
- package/src/utils/blockTag.ts +9 -4
|
@@ -229,6 +229,53 @@ describe('Rebalancer', () => {
|
|
|
229
229
|
expect(results[0].success).to.be.false;
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
it('should log scaled route amounts using origin local units', async () => {
|
|
233
|
+
const ctx = createRebalancerTestContext(['ethereum']);
|
|
234
|
+
ctx.tokensByChainName.ethereum.scale = {
|
|
235
|
+
numerator: 1,
|
|
236
|
+
denominator: 1_000_000_000_000,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const logger = {
|
|
240
|
+
child: Sinon.stub(),
|
|
241
|
+
info: Sinon.stub(),
|
|
242
|
+
warn: Sinon.stub(),
|
|
243
|
+
error: Sinon.stub(),
|
|
244
|
+
};
|
|
245
|
+
logger.child.returns(logger);
|
|
246
|
+
|
|
247
|
+
const rebalancer = new Rebalancer(
|
|
248
|
+
ctx.warpCore,
|
|
249
|
+
ctx.chainMetadata,
|
|
250
|
+
ctx.tokensByChainName,
|
|
251
|
+
ctx.multiProvider as any,
|
|
252
|
+
createMockActionTracker(),
|
|
253
|
+
logger as any,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const route = buildTestMovableCollateralRoute({
|
|
257
|
+
origin: 'ethereum',
|
|
258
|
+
destination: 'arbitrum',
|
|
259
|
+
amount: 1_000_000n,
|
|
260
|
+
});
|
|
261
|
+
const results = await rebalancer.rebalance([route]);
|
|
262
|
+
|
|
263
|
+
expect(results).to.have.lengthOf(1);
|
|
264
|
+
expect(results[0].success).to.be.false;
|
|
265
|
+
const validationErrorCall = logger.error
|
|
266
|
+
.getCalls()
|
|
267
|
+
.find(
|
|
268
|
+
(call) =>
|
|
269
|
+
call.args[1] ===
|
|
270
|
+
'Route validation failed: destination token not found.',
|
|
271
|
+
);
|
|
272
|
+
expect(validationErrorCall).to.not.be.undefined;
|
|
273
|
+
expect(validationErrorCall!.args[0].amount).to.equal(1);
|
|
274
|
+
expect(validationErrorCall!.args[1]).to.equal(
|
|
275
|
+
'Route validation failed: destination token not found.',
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
232
279
|
it('should fail when signer is not a rebalancer', async () => {
|
|
233
280
|
const ctx = createRebalancerTestContext(['ethereum', 'arbitrum'], {
|
|
234
281
|
ethereum: { isRebalancer: false },
|
|
@@ -337,6 +384,43 @@ describe('Rebalancer', () => {
|
|
|
337
384
|
expect(results).to.have.lengthOf(1);
|
|
338
385
|
expect(results[0].success).to.be.false;
|
|
339
386
|
});
|
|
387
|
+
|
|
388
|
+
it('should denormalize canonical route amounts before quote and populate calls', async () => {
|
|
389
|
+
const ctx = createRebalancerTestContext(['ethereum', 'arbitrum']);
|
|
390
|
+
ctx.tokensByChainName.ethereum.scale = {
|
|
391
|
+
numerator: 1,
|
|
392
|
+
denominator: 1_000_000_000_000,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
sandbox.stub(HyperlaneCore, 'getDispatchedMessages').returns([
|
|
396
|
+
{
|
|
397
|
+
id: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
|
398
|
+
} as any,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const rebalancer = new Rebalancer(
|
|
402
|
+
ctx.warpCore,
|
|
403
|
+
ctx.chainMetadata,
|
|
404
|
+
ctx.tokensByChainName,
|
|
405
|
+
ctx.multiProvider as any,
|
|
406
|
+
createMockActionTracker(),
|
|
407
|
+
testLogger,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await rebalancer.rebalance([
|
|
411
|
+
buildTestMovableCollateralRoute({
|
|
412
|
+
amount: 1_000_000n,
|
|
413
|
+
}),
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
expect(ctx.adapters.ethereum.getRebalanceQuotes.calledOnce).to.be.true;
|
|
417
|
+
expect(
|
|
418
|
+
ctx.adapters.ethereum.getRebalanceQuotes.firstCall.args[3],
|
|
419
|
+
).to.equal(1_000_000_000_000_000_000n);
|
|
420
|
+
expect(
|
|
421
|
+
ctx.adapters.ethereum.populateRebalanceTx.firstCall.args[1],
|
|
422
|
+
).to.equal(1_000_000_000_000_000_000n);
|
|
423
|
+
});
|
|
340
424
|
});
|
|
341
425
|
|
|
342
426
|
describe('executeTransactions()', () => {
|
package/src/core/Rebalancer.ts
CHANGED
|
@@ -25,10 +25,16 @@ import { MovableCollateralRoute } from '../interfaces/IStrategy.js';
|
|
|
25
25
|
import { type Metrics } from '../metrics/Metrics.js';
|
|
26
26
|
import type { IActionTracker } from '../tracking/IActionTracker.js';
|
|
27
27
|
import type { RebalanceIntent } from '../tracking/types.js';
|
|
28
|
+
import {
|
|
29
|
+
denormalizeToLocal,
|
|
30
|
+
normalizeToCanonical,
|
|
31
|
+
} from '../utils/balanceUtils.js';
|
|
28
32
|
|
|
29
33
|
// Internal types with intentId for tracking
|
|
30
34
|
type InternalExecutionResult = MovableCollateralExecutionResult & {
|
|
31
35
|
intentId: string;
|
|
36
|
+
canonicalAmount?: bigint;
|
|
37
|
+
localAmount?: bigint;
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
type InternalRoute = MovableCollateralRoute & { intentId: string };
|
|
@@ -104,7 +110,10 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
104
110
|
if (token) {
|
|
105
111
|
this.metrics.recordRebalanceAmount(
|
|
106
112
|
result.route,
|
|
107
|
-
token.amount(
|
|
113
|
+
token.amount(
|
|
114
|
+
result.localAmount ??
|
|
115
|
+
denormalizeToLocal(result.route.amount, token),
|
|
116
|
+
),
|
|
108
117
|
);
|
|
109
118
|
}
|
|
110
119
|
}
|
|
@@ -150,7 +159,7 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
150
159
|
intentId,
|
|
151
160
|
origin: this.multiProvider.getDomainId(result.route.origin),
|
|
152
161
|
destination: this.multiProvider.getDomainId(result.route.destination),
|
|
153
|
-
amount: result.route.amount,
|
|
162
|
+
amount: result.canonicalAmount ?? result.route.amount,
|
|
154
163
|
type: 'rebalance_message',
|
|
155
164
|
messageId: result.messageId,
|
|
156
165
|
txHash: result.txHash,
|
|
@@ -264,8 +273,9 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
264
273
|
const originToken = this.tokensByChainName[origin];
|
|
265
274
|
const destinationToken = this.tokensByChainName[destination];
|
|
266
275
|
const destinationChainMeta = this.chainMetadata[destination];
|
|
276
|
+
const localAmount = denormalizeToLocal(amount, originToken);
|
|
267
277
|
|
|
268
|
-
const originTokenAmount = originToken.amount(
|
|
278
|
+
const originTokenAmount = originToken.amount(localAmount);
|
|
269
279
|
const decimalFormattedAmount =
|
|
270
280
|
originTokenAmount.getDecimalFormattedAmount();
|
|
271
281
|
const originHypAdapter = originToken.getHypAdapter(
|
|
@@ -281,7 +291,7 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
281
291
|
bridge,
|
|
282
292
|
destinationChainMeta.domainId,
|
|
283
293
|
destinationToken.addressOrDenom,
|
|
284
|
-
|
|
294
|
+
localAmount,
|
|
285
295
|
);
|
|
286
296
|
} catch (error) {
|
|
287
297
|
this.logger.error(
|
|
@@ -302,7 +312,7 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
302
312
|
try {
|
|
303
313
|
populatedTx = await originHypAdapter.populateRebalanceTx(
|
|
304
314
|
destinationChainMeta.domainId,
|
|
305
|
-
|
|
315
|
+
localAmount,
|
|
306
316
|
bridge,
|
|
307
317
|
quotes,
|
|
308
318
|
);
|
|
@@ -337,7 +347,8 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
337
347
|
return false;
|
|
338
348
|
}
|
|
339
349
|
|
|
340
|
-
const
|
|
350
|
+
const localAmount = denormalizeToLocal(amount, originToken);
|
|
351
|
+
const originTokenAmount = originToken.amount(localAmount);
|
|
341
352
|
const decimalFormattedAmount =
|
|
342
353
|
originTokenAmount.getDecimalFormattedAmount();
|
|
343
354
|
|
|
@@ -676,6 +687,11 @@ export class Rebalancer implements IMovableCollateralRebalancer {
|
|
|
676
687
|
success: true,
|
|
677
688
|
messageId: dispatchedMessages[0].id,
|
|
678
689
|
txHash: receipt.transactionHash,
|
|
690
|
+
canonicalAmount: normalizeToCanonical(
|
|
691
|
+
transaction.originTokenAmount.amount,
|
|
692
|
+
transaction.originTokenAmount.token,
|
|
693
|
+
),
|
|
694
|
+
localAmount: transaction.originTokenAmount.amount,
|
|
679
695
|
};
|
|
680
696
|
}
|
|
681
697
|
|
|
@@ -646,7 +646,6 @@ describe('RebalancerOrchestrator', () => {
|
|
|
646
646
|
expect(inventoryRebalancer.rebalance.calledWith([])).to.be.false;
|
|
647
647
|
});
|
|
648
648
|
|
|
649
|
-
// eslint-disable-next-line jest/expect-expect
|
|
650
649
|
it('should NOT call inventoryRebalancer.rebalance([]) when inventoryRebalancer is not in rebalancers', async () => {
|
|
651
650
|
const strategy = createMockStrategy();
|
|
652
651
|
strategy.getRebalancingRoutes.returns([]);
|
|
@@ -366,6 +366,41 @@ describe('RebalancerService', () => {
|
|
|
366
366
|
expect(calledRoutes[0].destination).to.equal('arbitrum');
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
it('should normalize manual amount to canonical units when token has scale', async () => {
|
|
370
|
+
const rebalancer = createMockRebalancer();
|
|
371
|
+
const warpCore = {
|
|
372
|
+
tokens: [
|
|
373
|
+
{
|
|
374
|
+
...createMockToken('ethereum'),
|
|
375
|
+
decimals: 18,
|
|
376
|
+
scale: { numerator: 1n, denominator: 1_000_000_000_000n },
|
|
377
|
+
},
|
|
378
|
+
createMockToken('arbitrum'),
|
|
379
|
+
],
|
|
380
|
+
multiProvider: createMockMultiProvider(),
|
|
381
|
+
} as unknown as WarpCore;
|
|
382
|
+
|
|
383
|
+
const contextFactory = createMockContextFactory({ rebalancer, warpCore });
|
|
384
|
+
sandbox.stub(RebalancerContextFactory, 'create').resolves(contextFactory);
|
|
385
|
+
|
|
386
|
+
const service = new RebalancerService(
|
|
387
|
+
createMockMultiProvider(),
|
|
388
|
+
undefined,
|
|
389
|
+
{} as any,
|
|
390
|
+
createMockRebalancerConfig(),
|
|
391
|
+
{ mode: 'manual', logger: testLogger },
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
await service.executeManual({
|
|
395
|
+
origin: 'ethereum',
|
|
396
|
+
destination: 'arbitrum',
|
|
397
|
+
amount: '1',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const calledRoutes = rebalancer.rebalance.firstCall.args[0];
|
|
401
|
+
expect(calledRoutes[0].amount).to.equal(1_000_000n);
|
|
402
|
+
});
|
|
403
|
+
|
|
369
404
|
it('should throw when origin token not found', async () => {
|
|
370
405
|
const warpCore = {
|
|
371
406
|
tokens: [createMockToken('arbitrum')],
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Logger } from 'pino';
|
|
2
2
|
|
|
3
3
|
import { IRegistry } from '@hyperlane-xyz/registry';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
type MultiProtocolProvider,
|
|
6
|
+
type MultiProvider,
|
|
7
|
+
Token,
|
|
8
|
+
} from '@hyperlane-xyz/sdk';
|
|
9
|
+
import { ProtocolType, assert } from '@hyperlane-xyz/utils';
|
|
7
10
|
|
|
8
11
|
import { RebalancerConfig } from '../config/RebalancerConfig.js';
|
|
9
12
|
import {
|
|
@@ -27,6 +30,7 @@ import { Metrics } from '../metrics/Metrics.js';
|
|
|
27
30
|
import { type InventoryMonitorConfig, Monitor } from '../monitor/Monitor.js';
|
|
28
31
|
import type { IActionTracker } from '../tracking/IActionTracker.js';
|
|
29
32
|
import { InflightContextAdapter } from '../tracking/InflightContextAdapter.js';
|
|
33
|
+
import { normalizeConfiguredAmount } from '../utils/balanceUtils.js';
|
|
30
34
|
|
|
31
35
|
import type { RebalancerOrchestrator } from './RebalancerOrchestrator.js';
|
|
32
36
|
|
|
@@ -121,7 +125,7 @@ export class RebalancerService {
|
|
|
121
125
|
private orchestrator?: RebalancerOrchestrator;
|
|
122
126
|
constructor(
|
|
123
127
|
private readonly multiProvider: MultiProvider,
|
|
124
|
-
private readonly multiProtocolProvider:
|
|
128
|
+
private readonly multiProtocolProvider: MultiProtocolProvider | undefined,
|
|
125
129
|
private readonly registry: IRegistry,
|
|
126
130
|
private readonly rebalancerConfig: RebalancerConfig,
|
|
127
131
|
private readonly config: RebalancerServiceConfig,
|
|
@@ -293,7 +297,7 @@ export class RebalancerService {
|
|
|
293
297
|
const manualRoute: MovableCollateralRoute & { intentId: string } = {
|
|
294
298
|
origin,
|
|
295
299
|
destination,
|
|
296
|
-
amount:
|
|
300
|
+
amount: normalizeConfiguredAmount(amount, originToken),
|
|
297
301
|
executionType: 'movableCollateral',
|
|
298
302
|
bridge,
|
|
299
303
|
intentId: `manual-${Date.now()}`,
|
|
@@ -25,9 +25,9 @@ export async function getFirstMonitorEvent(
|
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
26
|
let settled = false;
|
|
27
27
|
|
|
28
|
-
async function finalize(
|
|
29
|
-
cb: (v:
|
|
30
|
-
value:
|
|
28
|
+
async function finalize<T>(
|
|
29
|
+
cb: (v: T) => void,
|
|
30
|
+
value: T,
|
|
31
31
|
timer: ReturnType<typeof setTimeout>,
|
|
32
32
|
) {
|
|
33
33
|
if (settled) return;
|
|
@@ -247,6 +247,78 @@ describe('RebalancerContextFactory', () => {
|
|
|
247
247
|
expect(providerChains).to.include('arbitrum');
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
it('should fail fast when bridgeMinAcceptedAmount is configured for a chain without a token', async () => {
|
|
251
|
+
const { multiProvider } = createMockMultiProvider([
|
|
252
|
+
{ name: 'ethereum', protocol: ProtocolType.Ethereum },
|
|
253
|
+
{ name: 'arbitrum', protocol: ProtocolType.Ethereum },
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const config = createMockConfig();
|
|
257
|
+
config.strategyConfig[0].chains.arbitrum.bridgeMinAcceptedAmount = 1;
|
|
258
|
+
|
|
259
|
+
let error: Error | undefined;
|
|
260
|
+
try {
|
|
261
|
+
const factory = await createFactory(config, multiProvider, {
|
|
262
|
+
tokens: [
|
|
263
|
+
createToken(
|
|
264
|
+
'ethereum',
|
|
265
|
+
TEST_ADDRESSES.ethereum,
|
|
266
|
+
TokenStandard.EvmHypCollateral,
|
|
267
|
+
),
|
|
268
|
+
],
|
|
269
|
+
} as WarpCoreConfig);
|
|
270
|
+
await factory.createStrategy();
|
|
271
|
+
} catch (caught) {
|
|
272
|
+
error = caught as Error;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(error?.message).to.equal(
|
|
276
|
+
'No token found for configured strategy chain arbitrum in warp route USDC/paradex',
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should fail fast when bridged supply is unavailable during initial collateral calculation', async () => {
|
|
281
|
+
const { multiProvider } = createMockMultiProvider([
|
|
282
|
+
{ name: 'ethereum', protocol: ProtocolType.Ethereum },
|
|
283
|
+
{ name: 'arbitrum', protocol: ProtocolType.Ethereum },
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const factory = await createFactory(createMockConfig(), multiProvider, {
|
|
287
|
+
tokens: [
|
|
288
|
+
createToken(
|
|
289
|
+
'ethereum',
|
|
290
|
+
TEST_ADDRESSES.ethereum,
|
|
291
|
+
TokenStandard.EvmHypCollateral,
|
|
292
|
+
),
|
|
293
|
+
createToken(
|
|
294
|
+
'arbitrum',
|
|
295
|
+
TEST_ADDRESSES.arbitrum,
|
|
296
|
+
TokenStandard.EvmHypSynthetic,
|
|
297
|
+
),
|
|
298
|
+
],
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const collateralToken = factory
|
|
302
|
+
.getWarpCore()
|
|
303
|
+
.tokens.find((token) => token.chainName === 'ethereum');
|
|
304
|
+
assert(collateralToken, 'Expected ethereum collateral token in test');
|
|
305
|
+
|
|
306
|
+
sandbox.stub(collateralToken, 'getHypAdapter').returns({
|
|
307
|
+
getBridgedSupply: sandbox.stub().resolves(undefined),
|
|
308
|
+
} as any);
|
|
309
|
+
|
|
310
|
+
let error: Error | undefined;
|
|
311
|
+
try {
|
|
312
|
+
await factory.createStrategy();
|
|
313
|
+
} catch (caught) {
|
|
314
|
+
error = caught as Error;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
expect(error?.message).to.equal(
|
|
318
|
+
'Missing bridged supply for ethereum while computing initial total collateral for warp route USDC/paradex',
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
250
322
|
it('should fail early when inventory override origin protocol signer key is missing', async () => {
|
|
251
323
|
const sealevelChain = 'solana';
|
|
252
324
|
const evmChain = 'ethereum';
|
|
@@ -310,13 +382,70 @@ describe('RebalancerContextFactory', () => {
|
|
|
310
382
|
: ProtocolType.Ethereum,
|
|
311
383
|
}));
|
|
312
384
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
385
|
+
let error: Error | undefined;
|
|
386
|
+
try {
|
|
387
|
+
await (factory as any).createInventoryRebalancerAndConfig(
|
|
388
|
+
{} as any,
|
|
389
|
+
{},
|
|
390
|
+
);
|
|
391
|
+
} catch (caught) {
|
|
392
|
+
error = caught as Error;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
expect(error?.message).to.contain(
|
|
316
396
|
`Missing inventory signer key for protocol ${ProtocolType.Sealevel}`,
|
|
317
397
|
);
|
|
318
398
|
});
|
|
319
399
|
|
|
400
|
+
it('should fail early when an inventory-relevant chain has no token', async () => {
|
|
401
|
+
const { multiProvider } = createMockMultiProvider([
|
|
402
|
+
{ name: 'ethereum', protocol: ProtocolType.Ethereum },
|
|
403
|
+
{ name: 'arbitrum', protocol: ProtocolType.Ethereum },
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
const config = {
|
|
407
|
+
...createMockConfig(),
|
|
408
|
+
inventorySigners: {
|
|
409
|
+
[ProtocolType.Ethereum]: {
|
|
410
|
+
address: TEST_ADDRESSES.ethereum,
|
|
411
|
+
key: '0xabc123',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
} as RebalancerConfig;
|
|
415
|
+
config.strategyConfig[0].chains.arbitrum.executionType =
|
|
416
|
+
ExecutionType.Inventory;
|
|
417
|
+
|
|
418
|
+
const factory = await createFactory(config, multiProvider, {
|
|
419
|
+
tokens: [
|
|
420
|
+
createToken(
|
|
421
|
+
'ethereum',
|
|
422
|
+
TEST_ADDRESSES.ethereum,
|
|
423
|
+
TokenStandard.EvmHypCollateral,
|
|
424
|
+
),
|
|
425
|
+
],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const getChainMetadataStub = factory.getWarpCore().multiProvider
|
|
429
|
+
.getChainMetadata as Sinon.SinonStub;
|
|
430
|
+
getChainMetadataStub.callsFake(() => ({
|
|
431
|
+
protocol: ProtocolType.Ethereum,
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
let error: Error | undefined;
|
|
435
|
+
try {
|
|
436
|
+
await (factory as any).createInventoryRebalancerAndConfig(
|
|
437
|
+
{} as any,
|
|
438
|
+
{},
|
|
439
|
+
);
|
|
440
|
+
} catch (caught) {
|
|
441
|
+
error = caught as Error;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
expect(error?.message).to.equal(
|
|
445
|
+
'No token found for inventory-relevant chain arbitrum in warp route USDC/paradex',
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
320
449
|
it('should fail early when inventory chain uses unsupported protocol', async () => {
|
|
321
450
|
const cosmosChain = 'cosmoshub';
|
|
322
451
|
const evmChain = 'ethereum';
|
|
@@ -384,9 +513,17 @@ describe('RebalancerContextFactory', () => {
|
|
|
384
513
|
: ProtocolType.Ethereum,
|
|
385
514
|
}));
|
|
386
515
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
516
|
+
let error: Error | undefined;
|
|
517
|
+
try {
|
|
518
|
+
await (factory as any).createInventoryRebalancerAndConfig(
|
|
519
|
+
{} as any,
|
|
520
|
+
{},
|
|
521
|
+
);
|
|
522
|
+
} catch (caught) {
|
|
523
|
+
error = caught as Error;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
expect(error?.message).to.contain(
|
|
390
527
|
`Inventory rebalancing does not support protocol '${ProtocolType.Cosmos}'`,
|
|
391
528
|
);
|
|
392
529
|
});
|
|
@@ -11,14 +11,7 @@ import {
|
|
|
11
11
|
WarpCore,
|
|
12
12
|
type WarpCoreConfig,
|
|
13
13
|
} from '@hyperlane-xyz/sdk';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
Address,
|
|
17
|
-
assert,
|
|
18
|
-
ProtocolType,
|
|
19
|
-
objMap,
|
|
20
|
-
toWei,
|
|
21
|
-
} from '@hyperlane-xyz/utils';
|
|
14
|
+
import { Address, assert, ProtocolType, objMap } from '@hyperlane-xyz/utils';
|
|
22
15
|
|
|
23
16
|
import { LiFiBridge } from '../bridges/LiFiBridge.js';
|
|
24
17
|
import { type RebalancerConfig } from '../config/RebalancerConfig.js';
|
|
@@ -64,6 +57,10 @@ import {
|
|
|
64
57
|
ExplorerClient,
|
|
65
58
|
type IExplorerClient,
|
|
66
59
|
} from '../utils/ExplorerClient.js';
|
|
60
|
+
import {
|
|
61
|
+
normalizeConfiguredAmount,
|
|
62
|
+
normalizeToCanonical,
|
|
63
|
+
} from '../utils/balanceUtils.js';
|
|
67
64
|
import { isCollateralizedTokenEligibleForRebalancing } from '../utils/tokenUtils.js';
|
|
68
65
|
|
|
69
66
|
const DEFAULT_EXPLORER_URL =
|
|
@@ -75,7 +72,7 @@ export class RebalancerContextFactory {
|
|
|
75
72
|
* @param warpCore - An instance of `WarpCore` configured for the specified `warpRouteId`.
|
|
76
73
|
* @param tokensByChainName - A map of chain->token to ease the lookup of token by chain
|
|
77
74
|
* @param multiProvider - MultiProvider instance (for movable collateral operations)
|
|
78
|
-
* @param multiProtocolProvider -
|
|
75
|
+
* @param multiProtocolProvider - MultiProtocolProvider instance (with mailbox metadata)
|
|
79
76
|
* @param registry - IRegistry instance
|
|
80
77
|
* @param logger - Logger instance
|
|
81
78
|
*/
|
|
@@ -84,7 +81,7 @@ export class RebalancerContextFactory {
|
|
|
84
81
|
private readonly warpCore: WarpCore,
|
|
85
82
|
private readonly tokensByChainName: ChainMap<Token>,
|
|
86
83
|
private readonly multiProvider: MultiProvider,
|
|
87
|
-
private readonly multiProtocolProvider:
|
|
84
|
+
private readonly multiProtocolProvider: MultiProtocolProvider,
|
|
88
85
|
private readonly registry: IRegistry,
|
|
89
86
|
private readonly logger: Logger,
|
|
90
87
|
private readonly inventorySignerKeysByProtocol?: Partial<
|
|
@@ -95,14 +92,14 @@ export class RebalancerContextFactory {
|
|
|
95
92
|
/**
|
|
96
93
|
* @param config - The rebalancer config
|
|
97
94
|
* @param multiProvider - MultiProvider instance (for movable collateral operations)
|
|
98
|
-
* @param multiProtocolProvider -
|
|
95
|
+
* @param multiProtocolProvider - MultiProtocolProvider instance (optional, created from multiProvider if not provided)
|
|
99
96
|
* @param registry - IRegistry instance
|
|
100
97
|
* @param logger - Logger instance
|
|
101
98
|
*/
|
|
102
99
|
public static async create(
|
|
103
100
|
config: RebalancerConfig,
|
|
104
101
|
multiProvider: MultiProvider,
|
|
105
|
-
multiProtocolProvider:
|
|
102
|
+
multiProtocolProvider: MultiProtocolProvider | undefined,
|
|
106
103
|
registry: IRegistry,
|
|
107
104
|
logger: Logger,
|
|
108
105
|
inventorySignerKeysByProtocol?: Partial<Record<ProtocolType, string>>,
|
|
@@ -147,7 +144,7 @@ export class RebalancerContextFactory {
|
|
|
147
144
|
multiProvider.getProvider(chain);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
// Create
|
|
147
|
+
// Create MultiProtocolProvider (convert from MultiProvider if not provided)
|
|
151
148
|
const mpp =
|
|
152
149
|
multiProtocolProvider ??
|
|
153
150
|
MultiProtocolProvider.fromMultiProvider(multiProvider);
|
|
@@ -253,9 +250,13 @@ export class RebalancerContextFactory {
|
|
|
253
250
|
);
|
|
254
251
|
if (chainConfig?.bridgeMinAcceptedAmount) {
|
|
255
252
|
const token = this.tokensByChainName[chainName];
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
253
|
+
assert(
|
|
254
|
+
token,
|
|
255
|
+
`No token found for configured strategy chain ${chainName} in warp route ${this.config.warpRouteId}`,
|
|
256
|
+
);
|
|
257
|
+
minAmountsByChain[chainName] = normalizeConfiguredAmount(
|
|
258
|
+
chainConfig.bridgeMinAcceptedAmount,
|
|
259
|
+
token,
|
|
259
260
|
);
|
|
260
261
|
}
|
|
261
262
|
}
|
|
@@ -460,6 +461,13 @@ export class RebalancerContextFactory {
|
|
|
460
461
|
return null;
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
for (const chain of allRelevantChains) {
|
|
465
|
+
assert(
|
|
466
|
+
this.tokensByChainName[chain],
|
|
467
|
+
`No token found for inventory-relevant chain ${chain} in warp route ${this.config.warpRouteId}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
463
471
|
const requiredProtocols = new Set(
|
|
464
472
|
allRelevantChains.map((chain) => {
|
|
465
473
|
const metadata = this.warpCore.multiProvider.getChainMetadata(chain);
|
|
@@ -762,7 +770,11 @@ export class RebalancerContextFactory {
|
|
|
762
770
|
) {
|
|
763
771
|
const adapter = token.getHypAdapter(this.warpCore.multiProvider);
|
|
764
772
|
const bridgedSupply = await adapter.getBridgedSupply();
|
|
765
|
-
|
|
773
|
+
assert(
|
|
774
|
+
bridgedSupply !== undefined,
|
|
775
|
+
`Missing bridged supply for ${token.chainName} while computing initial total collateral for warp route ${this.config.warpRouteId}`,
|
|
776
|
+
);
|
|
777
|
+
initialTotalCollateral += normalizeToCanonical(bridgedSupply, token);
|
|
766
778
|
}
|
|
767
779
|
}),
|
|
768
780
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type EvmMovableCollateralAdapter,
|
|
3
|
+
type IToken,
|
|
3
4
|
type TokenAmount,
|
|
4
5
|
} from '@hyperlane-xyz/sdk';
|
|
5
6
|
|
|
@@ -48,10 +49,12 @@ export type IInventoryRebalancer = IRebalancer<
|
|
|
48
49
|
InventoryExecutionResult
|
|
49
50
|
>;
|
|
50
51
|
|
|
52
|
+
type PreparedOriginTokenAmount = TokenAmount<IToken>;
|
|
53
|
+
|
|
51
54
|
export type PreparedTransaction = {
|
|
52
55
|
populatedTx: Awaited<
|
|
53
56
|
ReturnType<EvmMovableCollateralAdapter['populateRebalanceTx']>
|
|
54
57
|
>;
|
|
55
58
|
route: MovableCollateralRoute & { intentId: string };
|
|
56
|
-
originTokenAmount:
|
|
59
|
+
originTokenAmount: PreparedOriginTokenAmount;
|
|
57
60
|
};
|
package/src/monitor/Monitor.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type Token,
|
|
7
7
|
type WarpCore,
|
|
8
8
|
} from '@hyperlane-xyz/sdk';
|
|
9
|
-
import { Address, ProtocolType, sleep } from '@hyperlane-xyz/utils';
|
|
9
|
+
import { Address, ProtocolType, fromWei, sleep } from '@hyperlane-xyz/utils';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type ConfirmedBlockTag,
|
|
@@ -140,16 +140,29 @@ export class Monitor implements IMonitor {
|
|
|
140
140
|
const inventoryBalances = await this.fetchInventoryBalances();
|
|
141
141
|
if (Object.keys(inventoryBalances).length > 0) {
|
|
142
142
|
event.inventoryBalances = inventoryBalances;
|
|
143
|
+
const tokensByChain = new Map(
|
|
144
|
+
this.warpCore.tokens.map((token) => [token.chainName, token]),
|
|
145
|
+
);
|
|
146
|
+
// CAST: inventoryBalances keys come from configured monitor chains,
|
|
147
|
+
// but Object.entries widens them to string.
|
|
148
|
+
const inventoryBalanceEntries = Object.entries(
|
|
149
|
+
inventoryBalances,
|
|
150
|
+
) as [ChainName, bigint][];
|
|
143
151
|
this.logger.info(
|
|
144
152
|
{
|
|
145
153
|
chainsMonitored: Object.keys(inventoryBalances).length,
|
|
146
|
-
balances:
|
|
147
|
-
|
|
154
|
+
balances: inventoryBalanceEntries.map(([chain, balance]) => {
|
|
155
|
+
const token = tokensByChain.get(chain);
|
|
156
|
+
return {
|
|
148
157
|
chain,
|
|
158
|
+
tokenSymbol: token?.symbol,
|
|
149
159
|
balance: balance.toString(),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
balanceFormatted: fromWei(
|
|
161
|
+
balance.toString(),
|
|
162
|
+
token?.decimals,
|
|
163
|
+
),
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
153
166
|
},
|
|
154
167
|
'Inventory balances fetched',
|
|
155
168
|
);
|