@hyperlane-xyz/rebalancer 27.2.11 → 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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type Logger } from 'pino';
|
|
2
2
|
|
|
3
3
|
import type { ChainMap, ChainName, Token } from '@hyperlane-xyz/sdk';
|
|
4
|
-
import { toWei } from '@hyperlane-xyz/utils';
|
|
5
4
|
|
|
6
5
|
import type {
|
|
7
6
|
IStrategy,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
createStrategyRoute,
|
|
19
18
|
getBridgeConfig,
|
|
20
19
|
} from '../utils/bridgeUtils.js';
|
|
20
|
+
import { normalizeConfiguredAmount } from '../utils/balanceUtils.js';
|
|
21
21
|
|
|
22
22
|
export type Delta = { chain: ChainName; amount: bigint };
|
|
23
23
|
|
|
@@ -528,21 +528,24 @@ export abstract class BaseStrategy implements IStrategy {
|
|
|
528
528
|
route.origin,
|
|
529
529
|
route.destination,
|
|
530
530
|
);
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
this.logger.info(
|
|
536
|
-
{
|
|
537
|
-
context: this.constructor.name,
|
|
538
|
-
origin: route.origin,
|
|
539
|
-
destination: route.destination,
|
|
540
|
-
amount: route.amount.toString(),
|
|
541
|
-
minAmount: minAmount.toString(),
|
|
542
|
-
},
|
|
543
|
-
'Dropping route below bridgeMinAcceptedAmount',
|
|
531
|
+
if (bridgeConfig.bridgeMinAcceptedAmount != null) {
|
|
532
|
+
const minAmount = normalizeConfiguredAmount(
|
|
533
|
+
bridgeConfig.bridgeMinAcceptedAmount,
|
|
534
|
+
token,
|
|
544
535
|
);
|
|
545
|
-
|
|
536
|
+
if (route.amount < minAmount) {
|
|
537
|
+
this.logger.info(
|
|
538
|
+
{
|
|
539
|
+
context: this.constructor.name,
|
|
540
|
+
origin: route.origin,
|
|
541
|
+
destination: route.destination,
|
|
542
|
+
amount: route.amount.toString(),
|
|
543
|
+
minAmount: minAmount.toString(),
|
|
544
|
+
},
|
|
545
|
+
'Dropping route below bridgeMinAcceptedAmount',
|
|
546
|
+
);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
546
549
|
}
|
|
547
550
|
}
|
|
548
551
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Logger } from 'pino';
|
|
2
2
|
|
|
3
3
|
import { type ChainMap, type ChainName, type Token } from '@hyperlane-xyz/sdk';
|
|
4
|
-
import { toWei } from '@hyperlane-xyz/utils';
|
|
5
4
|
|
|
6
5
|
import {
|
|
7
6
|
type CollateralDeficitStrategyConfig,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
isInventoryConfig,
|
|
21
20
|
isMovableCollateralConfig,
|
|
22
21
|
} from '../utils/bridgeUtils.js';
|
|
22
|
+
import { normalizeConfiguredAmount } from '../utils/balanceUtils.js';
|
|
23
23
|
|
|
24
24
|
import { BaseStrategy, type Delta } from './BaseStrategy.js';
|
|
25
25
|
|
|
@@ -102,8 +102,9 @@ export class CollateralDeficitStrategy extends BaseStrategy {
|
|
|
102
102
|
for (const chain of this.chains) {
|
|
103
103
|
const balance = simulatedBalances[chain];
|
|
104
104
|
const token = this.getTokenByChainName(chain);
|
|
105
|
-
const bufferWei =
|
|
106
|
-
|
|
105
|
+
const bufferWei = normalizeConfiguredAmount(
|
|
106
|
+
this.config[chain].buffer,
|
|
107
|
+
token,
|
|
107
108
|
);
|
|
108
109
|
|
|
109
110
|
if (balance < 0n) {
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
} from '@hyperlane-xyz/sdk';
|
|
11
11
|
|
|
12
12
|
import { RebalancerMinAmountType } from '../config/types.js';
|
|
13
|
+
import { type MonitorEvent } from '../interfaces/IMonitor.js';
|
|
13
14
|
import type { RawBalances } from '../interfaces/IStrategy.js';
|
|
14
15
|
import { extractBridgeConfigs } from '../test/helpers.js';
|
|
16
|
+
import { getRawBalances } from '../utils/balanceUtils.js';
|
|
15
17
|
|
|
16
18
|
import { MinAmountStrategy } from './MinAmountStrategy.js';
|
|
17
19
|
|
|
@@ -64,7 +66,6 @@ describe('MinAmountStrategy', () => {
|
|
|
64
66
|
).to.throw('At least two chains must be configured');
|
|
65
67
|
});
|
|
66
68
|
|
|
67
|
-
// eslint-disable-next-line jest/expect-expect -- testing constructor doesn't throw
|
|
68
69
|
it('should create a strategy with minAmount and target using absolute values', () => {
|
|
69
70
|
new MinAmountStrategy(
|
|
70
71
|
{
|
|
@@ -94,7 +95,6 @@ describe('MinAmountStrategy', () => {
|
|
|
94
95
|
);
|
|
95
96
|
});
|
|
96
97
|
|
|
97
|
-
// eslint-disable-next-line jest/expect-expect -- testing constructor doesn't throw
|
|
98
98
|
it('should create a strategy with minAmount and target using relative values', () => {
|
|
99
99
|
new MinAmountStrategy(
|
|
100
100
|
{
|
|
@@ -415,6 +415,146 @@ describe('MinAmountStrategy', () => {
|
|
|
415
415
|
]);
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
+
it('should normalize absolute thresholds for mixed-decimal routes', () => {
|
|
419
|
+
const mixedTokensByChainName: ChainMap<Token> = {
|
|
420
|
+
[chain1]: new Token({
|
|
421
|
+
...tokenArgs,
|
|
422
|
+
chainName: chain1,
|
|
423
|
+
decimals: 18,
|
|
424
|
+
scale: { numerator: 1, denominator: 1_000_000_000_000 },
|
|
425
|
+
}),
|
|
426
|
+
[chain2]: new Token({
|
|
427
|
+
...tokenArgs,
|
|
428
|
+
chainName: chain2,
|
|
429
|
+
decimals: 6,
|
|
430
|
+
}),
|
|
431
|
+
};
|
|
432
|
+
const config = {
|
|
433
|
+
[chain1]: {
|
|
434
|
+
minAmount: {
|
|
435
|
+
min: '100',
|
|
436
|
+
target: '120',
|
|
437
|
+
type: RebalancerMinAmountType.Absolute,
|
|
438
|
+
},
|
|
439
|
+
bridge: AddressZero,
|
|
440
|
+
bridgeLockTime: 1,
|
|
441
|
+
},
|
|
442
|
+
[chain2]: {
|
|
443
|
+
minAmount: {
|
|
444
|
+
min: '100',
|
|
445
|
+
target: '120',
|
|
446
|
+
type: RebalancerMinAmountType.Absolute,
|
|
447
|
+
},
|
|
448
|
+
bridge: AddressZero,
|
|
449
|
+
bridgeLockTime: 1,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const strategy = new MinAmountStrategy(
|
|
454
|
+
config,
|
|
455
|
+
mixedTokensByChainName,
|
|
456
|
+
250_000_000n,
|
|
457
|
+
testLogger,
|
|
458
|
+
extractBridgeConfigs(config),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const routes = strategy.getRebalancingRoutes({
|
|
462
|
+
[chain1]: 50_000_000n,
|
|
463
|
+
[chain2]: 200_000_000n,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(routes).to.deep.equal([
|
|
467
|
+
{
|
|
468
|
+
origin: chain2,
|
|
469
|
+
destination: chain1,
|
|
470
|
+
amount: 70_000_000n,
|
|
471
|
+
bridge: AddressZero,
|
|
472
|
+
executionType: 'movableCollateral',
|
|
473
|
+
},
|
|
474
|
+
]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('normalizes mixed-decimal local balances before routing', () => {
|
|
478
|
+
const mixedTokensByChainName: ChainMap<Token> = {
|
|
479
|
+
[chain1]: new Token({
|
|
480
|
+
...tokenArgs,
|
|
481
|
+
chainName: chain1,
|
|
482
|
+
standard: TokenStandard.EvmHypCollateral,
|
|
483
|
+
decimals: 18,
|
|
484
|
+
scale: { numerator: 1, denominator: 1_000_000_000_000 },
|
|
485
|
+
}),
|
|
486
|
+
[chain2]: new Token({
|
|
487
|
+
...tokenArgs,
|
|
488
|
+
chainName: chain2,
|
|
489
|
+
standard: TokenStandard.EvmHypCollateral,
|
|
490
|
+
decimals: 6,
|
|
491
|
+
}),
|
|
492
|
+
};
|
|
493
|
+
const config = {
|
|
494
|
+
[chain1]: {
|
|
495
|
+
minAmount: {
|
|
496
|
+
min: '100',
|
|
497
|
+
target: '120',
|
|
498
|
+
type: RebalancerMinAmountType.Absolute,
|
|
499
|
+
},
|
|
500
|
+
bridge: AddressZero,
|
|
501
|
+
bridgeLockTime: 1,
|
|
502
|
+
},
|
|
503
|
+
[chain2]: {
|
|
504
|
+
minAmount: {
|
|
505
|
+
min: '100',
|
|
506
|
+
target: '120',
|
|
507
|
+
type: RebalancerMinAmountType.Absolute,
|
|
508
|
+
},
|
|
509
|
+
bridge: AddressZero,
|
|
510
|
+
bridgeLockTime: 1,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const strategy = new MinAmountStrategy(
|
|
515
|
+
config,
|
|
516
|
+
mixedTokensByChainName,
|
|
517
|
+
250_000_000n,
|
|
518
|
+
testLogger,
|
|
519
|
+
extractBridgeConfigs(config),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const event: MonitorEvent = {
|
|
523
|
+
tokensInfo: [
|
|
524
|
+
{
|
|
525
|
+
token: mixedTokensByChainName[chain1],
|
|
526
|
+
bridgedSupply: 50_000_000_000_000_000_000n,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
token: mixedTokensByChainName[chain2],
|
|
530
|
+
bridgedSupply: 200_000_000n,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
confirmedBlockTags: {
|
|
534
|
+
[chain1]: 1,
|
|
535
|
+
[chain2]: 1,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const rawBalances = getRawBalances([chain1, chain2], event, testLogger);
|
|
540
|
+
expect(rawBalances).to.deep.equal({
|
|
541
|
+
[chain1]: 50_000_000n,
|
|
542
|
+
[chain2]: 200_000_000n,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
546
|
+
|
|
547
|
+
expect(routes).to.deep.equal([
|
|
548
|
+
{
|
|
549
|
+
origin: chain2,
|
|
550
|
+
destination: chain1,
|
|
551
|
+
amount: 70_000_000n,
|
|
552
|
+
bridge: AddressZero,
|
|
553
|
+
executionType: 'movableCollateral',
|
|
554
|
+
},
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
418
558
|
it('should return multiple routes for multiple chains below minimum amount', () => {
|
|
419
559
|
const config = {
|
|
420
560
|
[chain1]: {
|
|
@@ -514,6 +654,102 @@ describe('MinAmountStrategy', () => {
|
|
|
514
654
|
);
|
|
515
655
|
});
|
|
516
656
|
|
|
657
|
+
it('should keep minAmount validation errors human-readable for mixed-decimal routes', () => {
|
|
658
|
+
const mixedTokensByChainName: ChainMap<Token> = {
|
|
659
|
+
[chain1]: new Token({
|
|
660
|
+
...tokenArgs,
|
|
661
|
+
chainName: chain1,
|
|
662
|
+
decimals: 18,
|
|
663
|
+
scale: { numerator: 1, denominator: 1_000_000_000_000 },
|
|
664
|
+
}),
|
|
665
|
+
[chain2]: new Token({
|
|
666
|
+
...tokenArgs,
|
|
667
|
+
chainName: chain2,
|
|
668
|
+
decimals: 6,
|
|
669
|
+
}),
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
expect(
|
|
673
|
+
() =>
|
|
674
|
+
new MinAmountStrategy(
|
|
675
|
+
{
|
|
676
|
+
[chain1]: {
|
|
677
|
+
minAmount: {
|
|
678
|
+
min: '100',
|
|
679
|
+
target: '120',
|
|
680
|
+
type: RebalancerMinAmountType.Absolute,
|
|
681
|
+
},
|
|
682
|
+
bridge: AddressZero,
|
|
683
|
+
bridgeLockTime: 1,
|
|
684
|
+
},
|
|
685
|
+
[chain2]: {
|
|
686
|
+
minAmount: {
|
|
687
|
+
min: '100',
|
|
688
|
+
target: '120',
|
|
689
|
+
type: RebalancerMinAmountType.Absolute,
|
|
690
|
+
},
|
|
691
|
+
bridge: AddressZero,
|
|
692
|
+
bridgeLockTime: 1,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
mixedTokensByChainName,
|
|
696
|
+
230_000_000n,
|
|
697
|
+
testLogger,
|
|
698
|
+
{},
|
|
699
|
+
),
|
|
700
|
+
).to.throw(
|
|
701
|
+
`Consider reducing the targets as the sum (240) is greater than sum of collaterals (230)`,
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('should keep fractional minAmount validation errors human-readable across heterogeneous decimals', () => {
|
|
706
|
+
const mixedTokensByChainName: ChainMap<Token> = {
|
|
707
|
+
[chain1]: new Token({
|
|
708
|
+
...tokenArgs,
|
|
709
|
+
chainName: chain1,
|
|
710
|
+
decimals: 6,
|
|
711
|
+
}),
|
|
712
|
+
[chain2]: new Token({
|
|
713
|
+
...tokenArgs,
|
|
714
|
+
chainName: chain2,
|
|
715
|
+
decimals: 18,
|
|
716
|
+
scale: { numerator: 1, denominator: 1_000_000_000_000 },
|
|
717
|
+
}),
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
expect(
|
|
721
|
+
() =>
|
|
722
|
+
new MinAmountStrategy(
|
|
723
|
+
{
|
|
724
|
+
[chain1]: {
|
|
725
|
+
minAmount: {
|
|
726
|
+
min: '100',
|
|
727
|
+
target: '120.25',
|
|
728
|
+
type: RebalancerMinAmountType.Absolute,
|
|
729
|
+
},
|
|
730
|
+
bridge: AddressZero,
|
|
731
|
+
bridgeLockTime: 1,
|
|
732
|
+
},
|
|
733
|
+
[chain2]: {
|
|
734
|
+
minAmount: {
|
|
735
|
+
min: '100',
|
|
736
|
+
target: '120.25',
|
|
737
|
+
type: RebalancerMinAmountType.Absolute,
|
|
738
|
+
},
|
|
739
|
+
bridge: AddressZero,
|
|
740
|
+
bridgeLockTime: 1,
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
mixedTokensByChainName,
|
|
744
|
+
230_500_000n,
|
|
745
|
+
testLogger,
|
|
746
|
+
{},
|
|
747
|
+
),
|
|
748
|
+
).to.throw(
|
|
749
|
+
`Consider reducing the targets as the sum (240.5) is greater than sum of collaterals (230.5)`,
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
|
|
517
753
|
it('should handle case where there is not enough surplus to meet all minimum requirements by scaling down deficits', () => {
|
|
518
754
|
const config = {
|
|
519
755
|
[chain1]: {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { BigNumber } from 'bignumber.js';
|
|
2
2
|
import { type Logger } from 'pino';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
localAmountFromMessage,
|
|
6
|
+
type ChainMap,
|
|
7
|
+
type Token,
|
|
8
|
+
} from '@hyperlane-xyz/sdk';
|
|
9
|
+
import { fromWei } from '@hyperlane-xyz/utils';
|
|
6
10
|
|
|
7
11
|
import {
|
|
8
12
|
type MinAmountStrategyConfig,
|
|
@@ -16,6 +20,7 @@ import type {
|
|
|
16
20
|
} from '../interfaces/IStrategy.js';
|
|
17
21
|
import { type Metrics } from '../metrics/Metrics.js';
|
|
18
22
|
import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
|
|
23
|
+
import { normalizeConfiguredAmount } from '../utils/balanceUtils.js';
|
|
19
24
|
|
|
20
25
|
import { BaseStrategy, type Delta } from './BaseStrategy.js';
|
|
21
26
|
|
|
@@ -116,9 +121,13 @@ export class MinAmountStrategy extends BaseStrategy {
|
|
|
116
121
|
if (chainConfig.minAmount.type === RebalancerMinAmountType.Absolute) {
|
|
117
122
|
const token = this.getTokenByChainName(chain);
|
|
118
123
|
|
|
119
|
-
minAmount =
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
minAmount = normalizeConfiguredAmount(
|
|
125
|
+
chainConfig.minAmount.min,
|
|
126
|
+
token,
|
|
127
|
+
);
|
|
128
|
+
targetAmount = normalizeConfiguredAmount(
|
|
129
|
+
chainConfig.minAmount.target,
|
|
130
|
+
token,
|
|
122
131
|
);
|
|
123
132
|
} else {
|
|
124
133
|
minAmount = BigInt(
|
|
@@ -172,29 +181,32 @@ export class MinAmountStrategy extends BaseStrategy {
|
|
|
172
181
|
|
|
173
182
|
if (minAmountType === RebalancerMinAmountType.Absolute) {
|
|
174
183
|
let totalTargets = 0n;
|
|
175
|
-
|
|
184
|
+
const displayToken = this.getTokenByChainName(this.chains[0]);
|
|
176
185
|
|
|
177
186
|
for (const chainName of this.chains) {
|
|
178
187
|
const token = this.getTokenByChainName(chainName);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
totalTargets += BigInt(
|
|
183
|
-
toWei(config[chainName].minAmount.target, token.decimals),
|
|
188
|
+
totalTargets += normalizeConfiguredAmount(
|
|
189
|
+
config[chainName].minAmount.target,
|
|
190
|
+
token,
|
|
184
191
|
);
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
if (totalTargets > totalCollateral) {
|
|
188
195
|
throw new Error(
|
|
189
|
-
`Consider reducing the targets as the sum (${
|
|
190
|
-
totalTargets
|
|
191
|
-
|
|
192
|
-
)}) is greater than sum of collaterals (${
|
|
193
|
-
totalCollateral
|
|
194
|
-
|
|
196
|
+
`Consider reducing the targets as the sum (${this.formatCanonicalAmount(
|
|
197
|
+
totalTargets,
|
|
198
|
+
displayToken,
|
|
199
|
+
)}) is greater than sum of collaterals (${this.formatCanonicalAmount(
|
|
200
|
+
totalCollateral,
|
|
201
|
+
displayToken,
|
|
195
202
|
)})`,
|
|
196
203
|
);
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
}
|
|
207
|
+
|
|
208
|
+
private formatCanonicalAmount(amount: bigint, token: Token): string {
|
|
209
|
+
const localAmount = localAmountFromMessage(amount, token.scale);
|
|
210
|
+
return fromWei(localAmount.toString(), token.decimals);
|
|
211
|
+
}
|
|
200
212
|
}
|
package/src/test/helpers.ts
CHANGED
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
type ChainMetadata,
|
|
7
7
|
type ChainName,
|
|
8
8
|
EvmMovableCollateralAdapter,
|
|
9
|
+
type IToken,
|
|
9
10
|
type InterchainGasQuote,
|
|
10
11
|
type MultiProvider,
|
|
11
12
|
type Token,
|
|
12
|
-
|
|
13
|
+
TokenAmount,
|
|
13
14
|
type WarpCore,
|
|
14
15
|
} from '@hyperlane-xyz/sdk';
|
|
15
16
|
|
|
@@ -115,17 +116,14 @@ export function buildTestPreparedTransaction(
|
|
|
115
116
|
|
|
116
117
|
// === Mock Factories ===
|
|
117
118
|
|
|
118
|
-
export function createMockTokenAmount(amount: bigint): TokenAmount {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
},
|
|
127
|
-
getDecimalFormattedAmount: () => ethers.utils.formatEther(amount),
|
|
128
|
-
} as unknown as TokenAmount;
|
|
119
|
+
export function createMockTokenAmount(amount: bigint): TokenAmount<IToken> {
|
|
120
|
+
const token = {
|
|
121
|
+
name: 'TestToken',
|
|
122
|
+
symbol: 'TEST',
|
|
123
|
+
decimals: 18,
|
|
124
|
+
addressOrDenom: TEST_ADDRESSES.token,
|
|
125
|
+
} as IToken;
|
|
126
|
+
return new TokenAmount(amount, token);
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
export interface MockAdapterConfig {
|
|
@@ -173,6 +171,7 @@ export interface MockTokenConfig {
|
|
|
173
171
|
name?: string;
|
|
174
172
|
decimals?: number;
|
|
175
173
|
addressOrDenom?: string;
|
|
174
|
+
scale?: Token['scale'];
|
|
176
175
|
adapter?: ReturnType<typeof createMockAdapter>;
|
|
177
176
|
}
|
|
178
177
|
|
|
@@ -181,6 +180,7 @@ export function createMockToken(config: MockTokenConfig = {}) {
|
|
|
181
180
|
name = 'TestToken',
|
|
182
181
|
decimals = 18,
|
|
183
182
|
addressOrDenom = TEST_ADDRESSES.token,
|
|
183
|
+
scale,
|
|
184
184
|
adapter = createMockAdapter(),
|
|
185
185
|
} = config;
|
|
186
186
|
|
|
@@ -188,6 +188,7 @@ export function createMockToken(config: MockTokenConfig = {}) {
|
|
|
188
188
|
name,
|
|
189
189
|
decimals,
|
|
190
190
|
addressOrDenom,
|
|
191
|
+
scale,
|
|
191
192
|
amount: (amt: bigint) => createMockTokenAmount(amt),
|
|
192
193
|
getHypAdapter: Sinon.stub().returns(adapter),
|
|
193
194
|
};
|
package/src/test/lifiMocks.ts
CHANGED
|
@@ -128,15 +128,18 @@ export function createMockBridgeQuote(
|
|
|
128
128
|
const route = overrides?.route as
|
|
129
129
|
| { action?: { fromChainId?: number; toChainId?: number } }
|
|
130
130
|
| undefined;
|
|
131
|
+
const usesReverseQuote = overrides?.requestParams?.toAmount !== undefined;
|
|
131
132
|
|
|
132
133
|
const defaultRequestParams: BridgeQuoteParams = {
|
|
133
134
|
fromChain: 42161,
|
|
134
135
|
toChain: 1399811149,
|
|
135
136
|
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
136
137
|
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
137
|
-
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
138
138
|
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
139
|
-
|
|
139
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
140
|
+
...(usesReverseQuote
|
|
141
|
+
? { toAmount: 9950000000n }
|
|
142
|
+
: { fromAmount: 10000000000n }),
|
|
140
143
|
};
|
|
141
144
|
const requestParams: BridgeQuoteParams = {
|
|
142
145
|
...defaultRequestParams,
|
|
@@ -807,6 +807,53 @@ describe('ActionTracker', () => {
|
|
|
807
807
|
expect(partialIntents[0].remaining).to.equal(600000000000000000n); // 0.6 ETH remaining
|
|
808
808
|
});
|
|
809
809
|
|
|
810
|
+
it('ignores inventory_movement amounts when computing remaining', async () => {
|
|
811
|
+
await rebalanceIntentStore.save({
|
|
812
|
+
id: 'partial-intent-with-movement',
|
|
813
|
+
status: 'in_progress',
|
|
814
|
+
origin: 1,
|
|
815
|
+
destination: 2,
|
|
816
|
+
amount: 1000000000000000000n,
|
|
817
|
+
executionMethod: 'inventory',
|
|
818
|
+
createdAt: Date.now(),
|
|
819
|
+
updatedAt: Date.now(),
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
await rebalanceActionStore.save({
|
|
823
|
+
id: 'action-deposit',
|
|
824
|
+
type: 'inventory_deposit',
|
|
825
|
+
status: 'complete',
|
|
826
|
+
intentId: 'partial-intent-with-movement',
|
|
827
|
+
origin: 1,
|
|
828
|
+
destination: 2,
|
|
829
|
+
amount: 400000000000000000n,
|
|
830
|
+
createdAt: Date.now(),
|
|
831
|
+
updatedAt: Date.now(),
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
await rebalanceActionStore.save({
|
|
835
|
+
id: 'action-movement',
|
|
836
|
+
type: 'inventory_movement',
|
|
837
|
+
status: 'complete',
|
|
838
|
+
intentId: 'partial-intent-with-movement',
|
|
839
|
+
origin: 3,
|
|
840
|
+
destination: 2,
|
|
841
|
+
amount: 9000000000000000000n,
|
|
842
|
+
createdAt: Date.now(),
|
|
843
|
+
updatedAt: Date.now(),
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const partialIntents =
|
|
847
|
+
await tracker.getPartiallyFulfilledInventoryIntents();
|
|
848
|
+
|
|
849
|
+
expect(partialIntents).to.have.lengthOf(1);
|
|
850
|
+
expect(partialIntents[0].intent.id).to.equal(
|
|
851
|
+
'partial-intent-with-movement',
|
|
852
|
+
);
|
|
853
|
+
expect(partialIntents[0].completedAmount).to.equal(400000000000000000n);
|
|
854
|
+
expect(partialIntents[0].remaining).to.equal(600000000000000000n);
|
|
855
|
+
});
|
|
856
|
+
|
|
810
857
|
it('does not return non-inventory intents', async () => {
|
|
811
858
|
// Create a not_started intent without executionMethod: 'inventory'
|
|
812
859
|
await rebalanceIntentStore.save({
|
|
@@ -636,7 +636,8 @@ export class ActionTracker implements IActionTracker {
|
|
|
636
636
|
);
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
//
|
|
639
|
+
// Only inventory_deposit amounts advance fulfillment. inventory_movement
|
|
640
|
+
// amounts stay in source-local units and only gate retries/stale cleanup.
|
|
640
641
|
const completedAmount = actions
|
|
641
642
|
.filter(
|
|
642
643
|
(a) => a.status === 'complete' && a.type === 'inventory_deposit',
|
|
@@ -6,7 +6,15 @@ import { type ChainName, type Token, TokenStandard } from '@hyperlane-xyz/sdk';
|
|
|
6
6
|
|
|
7
7
|
import { type MonitorEvent } from '../interfaces/IMonitor.js';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
alignLocalToCanonical,
|
|
11
|
+
denormalizeToLocal,
|
|
12
|
+
getRawBalances,
|
|
13
|
+
isIdentityScale,
|
|
14
|
+
getTokenScale,
|
|
15
|
+
normalizeConfiguredAmount,
|
|
16
|
+
normalizeToCanonical,
|
|
17
|
+
} from './balanceUtils.js';
|
|
10
18
|
|
|
11
19
|
const testLogger = pino({ level: 'silent' });
|
|
12
20
|
|
|
@@ -45,6 +53,16 @@ describe('getRawBalances', () => {
|
|
|
45
53
|
});
|
|
46
54
|
});
|
|
47
55
|
|
|
56
|
+
it('should normalize bridged supply to canonical units when token has scale', () => {
|
|
57
|
+
token.scale = { numerator: 1, denominator: 1_000_000_000_000 };
|
|
58
|
+
tokenBridgedSupply = 1_000_000_000_000_000_000n;
|
|
59
|
+
event.tokensInfo[0].bridgedSupply = tokenBridgedSupply;
|
|
60
|
+
|
|
61
|
+
expect(getRawBalances(chains, event, testLogger)).to.deep.equal({
|
|
62
|
+
mainnet: 1_000_000n,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
48
66
|
it('should return the bridged supply for the token (EvmHypNative)', () => {
|
|
49
67
|
token.standard = TokenStandard.EvmHypNative;
|
|
50
68
|
|
|
@@ -73,3 +91,71 @@ describe('getRawBalances', () => {
|
|
|
73
91
|
);
|
|
74
92
|
});
|
|
75
93
|
});
|
|
94
|
+
|
|
95
|
+
describe('scale helpers', () => {
|
|
96
|
+
const token = {
|
|
97
|
+
decimals: 18,
|
|
98
|
+
scale: { numerator: 1, denominator: 1_000_000_000_000 },
|
|
99
|
+
} as unknown as Token;
|
|
100
|
+
|
|
101
|
+
it('normalizes local amount to canonical amount', () => {
|
|
102
|
+
expect(normalizeToCanonical(1_000_000_000_000_000_000n, token)).to.equal(
|
|
103
|
+
1_000_000n,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('denormalizes canonical amount to local amount', () => {
|
|
108
|
+
expect(denormalizeToLocal(1_000_000n, token)).to.equal(
|
|
109
|
+
1_000_000_000_000_000_000n,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('aligns local amount to exact canonical progress', () => {
|
|
114
|
+
expect(alignLocalToCanonical(999_999_999_999n, token)).to.deep.equal({
|
|
115
|
+
localAmount: 0n,
|
|
116
|
+
messageAmount: 0n,
|
|
117
|
+
});
|
|
118
|
+
expect(
|
|
119
|
+
alignLocalToCanonical(1_000_000_000_000_500_000n, token),
|
|
120
|
+
).to.deep.equal({
|
|
121
|
+
localAmount: 1_000_000_000_000_000_000n,
|
|
122
|
+
messageAmount: 1_000_000n,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns identity scale when scale is undefined', () => {
|
|
127
|
+
expect(getTokenScale({} as Token)).to.deep.equal({
|
|
128
|
+
numerator: 1n,
|
|
129
|
+
denominator: 1n,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('treats equivalent ratios as identity scale', () => {
|
|
134
|
+
expect(isIdentityScale({} as Token)).to.be.true;
|
|
135
|
+
expect(
|
|
136
|
+
isIdentityScale({
|
|
137
|
+
scale: { numerator: 1, denominator: 1 },
|
|
138
|
+
} as Token),
|
|
139
|
+
).to.be.true;
|
|
140
|
+
expect(
|
|
141
|
+
isIdentityScale({
|
|
142
|
+
scale: { numerator: 5, denominator: 5 },
|
|
143
|
+
} as Token),
|
|
144
|
+
).to.be.true;
|
|
145
|
+
expect(
|
|
146
|
+
isIdentityScale({
|
|
147
|
+
scale: { numerator: 10, denominator: 20 },
|
|
148
|
+
} as Token),
|
|
149
|
+
).to.be.false;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('normalizes configured amount using token decimals and scale', () => {
|
|
153
|
+
expect(normalizeConfiguredAmount('1', token)).to.equal(1_000_000n);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('normalizes sub-unit local dust to zero canonical amount', () => {
|
|
157
|
+
expect(normalizeConfiguredAmount('0.000000000000000001', token)).to.equal(
|
|
158
|
+
0n,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|