@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
|
@@ -3,27 +3,19 @@ import type { Logger } from 'pino';
|
|
|
3
3
|
import {
|
|
4
4
|
type ChainName,
|
|
5
5
|
HyperlaneCore,
|
|
6
|
-
type InterchainGasQuote,
|
|
7
|
-
type IToken,
|
|
8
6
|
type MultiProtocolSignerSignerAccountInfo,
|
|
9
7
|
type MultiProvider,
|
|
10
|
-
Token,
|
|
11
8
|
ProviderType,
|
|
12
9
|
SealevelCoreAdapter,
|
|
13
10
|
TOKEN_COLLATERALIZED_STANDARDS,
|
|
14
|
-
|
|
11
|
+
type Token,
|
|
15
12
|
type WarpTypedTransaction,
|
|
16
13
|
type WarpCore,
|
|
17
14
|
WarpTxCategory,
|
|
18
15
|
getSignerForChain,
|
|
19
16
|
type TypedTransactionReceipt,
|
|
20
17
|
} from '@hyperlane-xyz/sdk';
|
|
21
|
-
import {
|
|
22
|
-
ProtocolType,
|
|
23
|
-
assert,
|
|
24
|
-
ensure0x,
|
|
25
|
-
isZeroishAddress,
|
|
26
|
-
} from '@hyperlane-xyz/utils';
|
|
18
|
+
import { ProtocolType, assert, ensure0x, fromWei } from '@hyperlane-xyz/utils';
|
|
27
19
|
|
|
28
20
|
import type { ExternalBridgeType } from '../config/types.js';
|
|
29
21
|
import type {
|
|
@@ -51,6 +43,11 @@ import {
|
|
|
51
43
|
} from '../utils/tokenUtils.js';
|
|
52
44
|
import { parseSolanaPrivateKey } from '../utils/solanaKeyParser.js';
|
|
53
45
|
import { toProtocolTransaction } from '../utils/transactionUtils.js';
|
|
46
|
+
import {
|
|
47
|
+
alignLocalToCanonical,
|
|
48
|
+
denormalizeToLocal,
|
|
49
|
+
normalizeToCanonical,
|
|
50
|
+
} from '../utils/balanceUtils.js';
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
53
|
* Buffer percentage to add when bridging inventory.
|
|
@@ -72,6 +69,71 @@ const GAS_COST_MULTIPLIER = 20n;
|
|
|
72
69
|
*/
|
|
73
70
|
const MAX_GAS_PERCENT_THRESHOLD = 10n;
|
|
74
71
|
|
|
72
|
+
type BridgeCapacity = {
|
|
73
|
+
maxSourceInput: bigint;
|
|
74
|
+
maxTargetOutput: bigint;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
|
|
78
|
+
'balance may be insufficient',
|
|
79
|
+
'transfer amount exceeds balance',
|
|
80
|
+
'insufficient balance',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
function hasRecoverableMaxTransferErrorMessage(message: string): boolean {
|
|
84
|
+
const normalized = message.toLowerCase();
|
|
85
|
+
return (
|
|
86
|
+
normalized.includes('unpredictable_gas_limit') ||
|
|
87
|
+
RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES.some((pattern) =>
|
|
88
|
+
normalized.includes(pattern),
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecoverableMaxTransferProbeError(error: unknown): boolean {
|
|
94
|
+
const seen = new Set<unknown>();
|
|
95
|
+
const stack: unknown[] = [error];
|
|
96
|
+
|
|
97
|
+
while (stack.length > 0) {
|
|
98
|
+
const current = stack.pop();
|
|
99
|
+
if (current == null) continue;
|
|
100
|
+
|
|
101
|
+
if (typeof current === 'string') {
|
|
102
|
+
if (hasRecoverableMaxTransferErrorMessage(current)) return true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof current !== 'object') continue;
|
|
107
|
+
if (seen.has(current)) continue;
|
|
108
|
+
seen.add(current);
|
|
109
|
+
|
|
110
|
+
const candidate = current as {
|
|
111
|
+
code?: unknown;
|
|
112
|
+
message?: unknown;
|
|
113
|
+
cause?: unknown;
|
|
114
|
+
error?: unknown;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
typeof candidate.code === 'string' &&
|
|
119
|
+
candidate.code.toUpperCase() === 'UNPREDICTABLE_GAS_LIMIT'
|
|
120
|
+
) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
typeof candidate.message === 'string' &&
|
|
126
|
+
hasRecoverableMaxTransferErrorMessage(candidate.message)
|
|
127
|
+
) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stack.push(candidate.cause, candidate.error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
75
137
|
/**
|
|
76
138
|
* Configuration for the InventoryRebalancer.
|
|
77
139
|
*/
|
|
@@ -288,6 +350,10 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
288
350
|
return total;
|
|
289
351
|
}
|
|
290
352
|
|
|
353
|
+
private formatLocalAmount(amount: bigint, token: Token): string {
|
|
354
|
+
return fromWei(amount.toString(), token.decimals);
|
|
355
|
+
}
|
|
356
|
+
|
|
291
357
|
/**
|
|
292
358
|
* Get the effective available inventory for a chain, accounting for
|
|
293
359
|
* inventory already consumed during this execution cycle.
|
|
@@ -533,12 +599,18 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
533
599
|
{
|
|
534
600
|
strategyRoute: `${origin} (surplus) → ${destination} (deficit)`,
|
|
535
601
|
executionRoute: `transferRemote FROM ${destination} TO ${origin}`,
|
|
536
|
-
|
|
602
|
+
canonicalAmount: amount.toString(),
|
|
537
603
|
intentId: intent.id,
|
|
538
604
|
},
|
|
539
605
|
'Executing inventory route',
|
|
540
606
|
);
|
|
541
607
|
|
|
608
|
+
const sourceToken = this.getTokenForChain(destination);
|
|
609
|
+
assert(sourceToken, `No token found for source chain: ${destination}`);
|
|
610
|
+
const requestedLocalAmount = denormalizeToLocal(amount, sourceToken);
|
|
611
|
+
const executionSender = this.getInventorySignerAddress(destination);
|
|
612
|
+
const executionRecipient = this.getInventorySignerAddress(origin);
|
|
613
|
+
|
|
542
614
|
// Check available inventory on the DESTINATION (deficit) chain
|
|
543
615
|
// We need inventory here because transferRemote is called FROM this chain
|
|
544
616
|
const availableInventory = this.getEffectiveAvailableInventory(destination);
|
|
@@ -547,9 +619,15 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
547
619
|
{
|
|
548
620
|
checkingChain: destination,
|
|
549
621
|
availableInventory: availableInventory.toString(),
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
622
|
+
availableInventoryFormatted: this.formatLocalAmount(
|
|
623
|
+
availableInventory,
|
|
624
|
+
sourceToken,
|
|
625
|
+
),
|
|
626
|
+
requiredAmount: requestedLocalAmount.toString(),
|
|
627
|
+
requiredAmountFormatted: this.formatLocalAmount(
|
|
628
|
+
requestedLocalAmount,
|
|
629
|
+
sourceToken,
|
|
630
|
+
),
|
|
553
631
|
},
|
|
554
632
|
'Checking effective inventory on destination (deficit) chain',
|
|
555
633
|
);
|
|
@@ -560,15 +638,72 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
560
638
|
destination, // FROM chain (where transferRemote is called)
|
|
561
639
|
origin, // TO chain (where Hyperlane message goes)
|
|
562
640
|
availableInventory,
|
|
563
|
-
|
|
641
|
+
requestedLocalAmount,
|
|
564
642
|
this.multiProvider,
|
|
565
643
|
this.warpCore.multiProvider,
|
|
566
644
|
this.getTokenForChain.bind(this),
|
|
567
|
-
|
|
645
|
+
executionSender,
|
|
568
646
|
isNativeTokenStandard,
|
|
569
647
|
this.logger,
|
|
570
648
|
);
|
|
571
|
-
const {
|
|
649
|
+
const { minViableTransfer } = costs;
|
|
650
|
+
let maxTransferable = costs.maxTransferable;
|
|
651
|
+
|
|
652
|
+
if (!isNativeTokenStandard(sourceToken.standard)) {
|
|
653
|
+
if (availableInventory === 0n) {
|
|
654
|
+
maxTransferable = 0n;
|
|
655
|
+
this.logger.debug(
|
|
656
|
+
{
|
|
657
|
+
fromChain: destination,
|
|
658
|
+
toChain: origin,
|
|
659
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
660
|
+
},
|
|
661
|
+
'Skipping fee-aware max transferable probe because destination inventory is zero',
|
|
662
|
+
);
|
|
663
|
+
} else {
|
|
664
|
+
try {
|
|
665
|
+
const feeAwareMaxTransfer = await this.warpCore.getMaxTransferAmount({
|
|
666
|
+
balance: sourceToken.amount(availableInventory),
|
|
667
|
+
destination: origin,
|
|
668
|
+
sender: executionSender,
|
|
669
|
+
recipient: executionRecipient,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
maxTransferable =
|
|
673
|
+
feeAwareMaxTransfer.amount < requestedLocalAmount
|
|
674
|
+
? feeAwareMaxTransfer.amount
|
|
675
|
+
: requestedLocalAmount;
|
|
676
|
+
|
|
677
|
+
this.logger.debug(
|
|
678
|
+
{
|
|
679
|
+
fromChain: destination,
|
|
680
|
+
toChain: origin,
|
|
681
|
+
availableInventory: availableInventory.toString(),
|
|
682
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
683
|
+
feeAwareMaxTransferable: maxTransferable.toString(),
|
|
684
|
+
},
|
|
685
|
+
'Calculated fee-aware max transferable amount for non-native route',
|
|
686
|
+
);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (!isRecoverableMaxTransferProbeError(error)) {
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
maxTransferable = 0n;
|
|
693
|
+
this.logger.warn(
|
|
694
|
+
{
|
|
695
|
+
fromChain: destination,
|
|
696
|
+
toChain: origin,
|
|
697
|
+
availableInventory: availableInventory.toString(),
|
|
698
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
699
|
+
error: error instanceof Error ? error.message : String(error),
|
|
700
|
+
intentId: intent.id,
|
|
701
|
+
},
|
|
702
|
+
'Fee-aware max transferable probe failed due to insufficient balance, falling back to external bridge',
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
572
707
|
|
|
573
708
|
// Calculate total inventory across all chains
|
|
574
709
|
// Note: consumedInventory tracking is handled separately within this cycle
|
|
@@ -578,24 +713,36 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
578
713
|
{
|
|
579
714
|
fromChain: destination,
|
|
580
715
|
toChain: origin,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
716
|
+
availableInventoryFormatted: this.formatLocalAmount(
|
|
717
|
+
availableInventory,
|
|
718
|
+
sourceToken,
|
|
719
|
+
),
|
|
720
|
+
requestedAmountFormatted: this.formatLocalAmount(
|
|
721
|
+
requestedLocalAmount,
|
|
722
|
+
sourceToken,
|
|
723
|
+
),
|
|
724
|
+
maxTransferableFormatted: this.formatLocalAmount(
|
|
725
|
+
maxTransferable,
|
|
726
|
+
sourceToken,
|
|
727
|
+
),
|
|
728
|
+
minViableTransferFormatted: this.formatLocalAmount(
|
|
729
|
+
minViableTransfer,
|
|
730
|
+
sourceToken,
|
|
731
|
+
),
|
|
732
|
+
canFullyFulfill: maxTransferable >= requestedLocalAmount,
|
|
587
733
|
canPartialFulfill: maxTransferable >= minViableTransfer,
|
|
734
|
+
totalInventory: totalInventory.toString(),
|
|
588
735
|
},
|
|
589
736
|
'Calculated max transferable amount with cost-based threshold',
|
|
590
737
|
);
|
|
591
738
|
|
|
592
739
|
// Early exit: If remaining amount is below minViableTransfer, complete the intent
|
|
593
740
|
// This prevents infinite loops when the remaining amount is too small to economically bridge
|
|
594
|
-
if (
|
|
741
|
+
if (requestedLocalAmount < minViableTransfer) {
|
|
595
742
|
this.logger.info(
|
|
596
743
|
{
|
|
597
744
|
intentId: intent.id,
|
|
598
|
-
amount:
|
|
745
|
+
amount: requestedLocalAmount.toString(),
|
|
599
746
|
minViableTransfer: minViableTransfer.toString(),
|
|
600
747
|
},
|
|
601
748
|
'Remaining amount below minViableTransfer, completing intent with acceptable loss',
|
|
@@ -616,227 +763,270 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
616
763
|
...route,
|
|
617
764
|
origin: destination, // transferRemote called FROM here
|
|
618
765
|
destination: origin, // Hyperlane message goes TO here
|
|
766
|
+
amount: requestedLocalAmount,
|
|
619
767
|
};
|
|
620
768
|
|
|
621
|
-
if (maxTransferable >=
|
|
769
|
+
if (maxTransferable >= requestedLocalAmount) {
|
|
622
770
|
// Sufficient inventory on destination - execute transferRemote directly
|
|
771
|
+
const fulfilledCanonicalAmount = normalizeToCanonical(
|
|
772
|
+
requestedLocalAmount,
|
|
773
|
+
sourceToken,
|
|
774
|
+
);
|
|
623
775
|
const result = await this.executeTransferRemote(
|
|
624
776
|
swappedRoute,
|
|
625
777
|
intent,
|
|
626
|
-
|
|
778
|
+
fulfilledCanonicalAmount,
|
|
627
779
|
);
|
|
628
780
|
// Return original strategy route in result (not the swapped execution route)
|
|
629
781
|
return { ...result, route };
|
|
630
782
|
} else if (maxTransferable > 0n && maxTransferable >= minViableTransfer) {
|
|
631
783
|
// Partial transfer: Transfer available inventory when economically viable
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
};
|
|
636
|
-
const result = await this.executeTransferRemote(
|
|
637
|
-
partialSwappedRoute,
|
|
638
|
-
intent,
|
|
639
|
-
costs.gasQuote!,
|
|
640
|
-
);
|
|
641
|
-
|
|
642
|
-
this.logger.info(
|
|
643
|
-
{
|
|
644
|
-
intentId: intent.id,
|
|
645
|
-
partialAmount: maxTransferable.toString(),
|
|
646
|
-
requestedAmount: amount.toString(),
|
|
647
|
-
remainingAmount: (amount - maxTransferable).toString(),
|
|
648
|
-
},
|
|
649
|
-
'Executed partial inventory deposit, remaining will be handled in future cycles',
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
// Return original strategy route in result (not the swapped execution route)
|
|
653
|
-
return { ...result, route };
|
|
654
|
-
} else {
|
|
655
|
-
// Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
|
|
656
|
-
this.logger.info(
|
|
657
|
-
{
|
|
658
|
-
targetChain: destination,
|
|
659
|
-
maxTransferable: maxTransferable.toString(),
|
|
660
|
-
minViableTransfer: minViableTransfer.toString(),
|
|
661
|
-
costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
|
|
662
|
-
intentId: intent.id,
|
|
663
|
-
},
|
|
664
|
-
'Inventory below cost-based threshold on destination, triggering LiFi movement',
|
|
784
|
+
const alignedExecution = alignLocalToCanonical(
|
|
785
|
+
maxTransferable,
|
|
786
|
+
sourceToken,
|
|
665
787
|
);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const allSources = this.selectAllSourceChains(destination);
|
|
669
|
-
|
|
670
|
-
if (allSources.length === 0) {
|
|
671
|
-
this.logger.warn(
|
|
788
|
+
if (alignedExecution.messageAmount === 0n) {
|
|
789
|
+
this.logger.info(
|
|
672
790
|
{
|
|
673
|
-
origin,
|
|
674
|
-
destination,
|
|
675
|
-
amount: amount.toString(),
|
|
676
791
|
intentId: intent.id,
|
|
792
|
+
maxTransferable: maxTransferable.toString(),
|
|
677
793
|
},
|
|
678
|
-
'
|
|
794
|
+
'Skipping partial transferRemote because available local amount cannot produce canonical progress',
|
|
679
795
|
);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
error: 'No inventory available on any monitored chain',
|
|
796
|
+
} else {
|
|
797
|
+
const partialSwappedRoute: InventoryRoute = {
|
|
798
|
+
...swappedRoute,
|
|
799
|
+
amount: alignedExecution.localAmount,
|
|
685
800
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const viableSources: Array<{ chain: ChainName; maxViable: bigint }> = [];
|
|
691
|
-
|
|
692
|
-
for (const source of allSources) {
|
|
693
|
-
const maxViable = await this.calculateMaxViableBridgeAmount(
|
|
694
|
-
source.chain,
|
|
695
|
-
destination,
|
|
696
|
-
source.availableAmount,
|
|
697
|
-
route.externalBridge,
|
|
801
|
+
const result = await this.executeTransferRemote(
|
|
802
|
+
partialSwappedRoute,
|
|
803
|
+
intent,
|
|
804
|
+
alignedExecution.messageAmount,
|
|
698
805
|
);
|
|
699
806
|
|
|
700
|
-
|
|
701
|
-
viableSources.push({ chain: source.chain, maxViable });
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Sort by max viable descending (bridge from largest sources first)
|
|
706
|
-
viableSources.sort((a, b) => (a.maxViable > b.maxViable ? -1 : 1));
|
|
707
|
-
|
|
708
|
-
if (viableSources.length === 0) {
|
|
709
|
-
this.logger.warn(
|
|
807
|
+
this.logger.info(
|
|
710
808
|
{
|
|
711
|
-
targetChain: destination,
|
|
712
|
-
sourcesChecked: allSources.length,
|
|
713
809
|
intentId: intent.id,
|
|
810
|
+
partialAmount: alignedExecution.localAmount.toString(),
|
|
811
|
+
partialAmountCanonical: alignedExecution.messageAmount.toString(),
|
|
812
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
813
|
+
requestedAmountCanonical: amount.toString(),
|
|
814
|
+
remainingAmountCanonical: (amount > alignedExecution.messageAmount
|
|
815
|
+
? amount - alignedExecution.messageAmount
|
|
816
|
+
: 0n
|
|
817
|
+
).toString(),
|
|
714
818
|
},
|
|
715
|
-
'
|
|
819
|
+
'Executed partial inventory deposit, remaining will be handled in future cycles',
|
|
716
820
|
);
|
|
717
821
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
success: false,
|
|
721
|
-
error: 'No viable bridge sources available',
|
|
722
|
-
};
|
|
822
|
+
// Return original strategy route in result (not the swapped execution route)
|
|
823
|
+
return { ...result, route };
|
|
723
824
|
}
|
|
825
|
+
}
|
|
724
826
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
source.maxViable >= remaining ? remaining : source.maxViable; // Already gas-adjusted!
|
|
827
|
+
// Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
|
|
828
|
+
this.logger.info(
|
|
829
|
+
{
|
|
830
|
+
targetChain: destination,
|
|
831
|
+
maxTransferable: maxTransferable.toString(),
|
|
832
|
+
minViableTransfer: minViableTransfer.toString(),
|
|
833
|
+
costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
|
|
834
|
+
intentId: intent.id,
|
|
835
|
+
},
|
|
836
|
+
'Inventory below cost-based threshold on destination, triggering LiFi movement',
|
|
837
|
+
);
|
|
737
838
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
amount: amountFromSource,
|
|
741
|
-
});
|
|
742
|
-
totalPlanned += amountFromSource;
|
|
743
|
-
}
|
|
839
|
+
// Get all available source chains with raw inventory
|
|
840
|
+
const allSources = this.selectAllSourceChains(destination);
|
|
744
841
|
|
|
745
|
-
|
|
842
|
+
if (allSources.length === 0) {
|
|
843
|
+
this.logger.warn(
|
|
746
844
|
{
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
maxViable: s.maxViable.toString(),
|
|
751
|
-
maxViableEth: (Number(s.maxViable) / 1e18).toFixed(6),
|
|
752
|
-
})),
|
|
753
|
-
bridgePlans: bridgePlans.map((p) => ({
|
|
754
|
-
chain: p.chain,
|
|
755
|
-
amount: p.amount.toString(),
|
|
756
|
-
amountEth: (Number(p.amount) / 1e18).toFixed(6),
|
|
757
|
-
})),
|
|
758
|
-
totalPlanned: totalPlanned.toString(),
|
|
759
|
-
targetWithBuffer: targetWithBuffer.toString(),
|
|
845
|
+
origin,
|
|
846
|
+
destination,
|
|
847
|
+
amount: requestedLocalAmount.toString(),
|
|
760
848
|
intentId: intent.id,
|
|
761
849
|
},
|
|
762
|
-
'
|
|
850
|
+
'No inventory available on any monitored chain',
|
|
763
851
|
);
|
|
764
852
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
plan.amount,
|
|
772
|
-
intent,
|
|
773
|
-
route.externalBridge,
|
|
774
|
-
),
|
|
775
|
-
),
|
|
776
|
-
);
|
|
853
|
+
return {
|
|
854
|
+
route,
|
|
855
|
+
success: false,
|
|
856
|
+
error: 'No inventory available on any monitored chain',
|
|
857
|
+
};
|
|
858
|
+
}
|
|
777
859
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
860
|
+
// Calculate source capacities in destination-local units.
|
|
861
|
+
const viableSources: Array<{
|
|
862
|
+
chain: ChainName;
|
|
863
|
+
maxSourceInput: bigint;
|
|
864
|
+
maxTargetOutput: bigint;
|
|
865
|
+
}> = [];
|
|
782
866
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
867
|
+
for (const source of allSources) {
|
|
868
|
+
const capacity = await this.calculateBridgeCapacity(
|
|
869
|
+
source.chain,
|
|
870
|
+
destination,
|
|
871
|
+
source.availableAmount,
|
|
872
|
+
route.externalBridge,
|
|
873
|
+
);
|
|
786
874
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
totalBridged += plan.amount;
|
|
790
|
-
this.logger.info(
|
|
791
|
-
{
|
|
792
|
-
sourceChain: plan.chain,
|
|
793
|
-
amount: plan.amount.toString(),
|
|
794
|
-
txHash: result.value.txHash,
|
|
795
|
-
},
|
|
796
|
-
'Inventory movement succeeded',
|
|
797
|
-
);
|
|
798
|
-
} else {
|
|
799
|
-
const error =
|
|
800
|
-
result.status === 'rejected'
|
|
801
|
-
? result.reason?.message
|
|
802
|
-
: result.value.error;
|
|
803
|
-
if (error) {
|
|
804
|
-
failedErrors.push(`${plan.chain}: ${error}`);
|
|
805
|
-
}
|
|
806
|
-
this.logger.warn(
|
|
807
|
-
{
|
|
808
|
-
sourceChain: plan.chain,
|
|
809
|
-
amount: plan.amount.toString(),
|
|
810
|
-
error,
|
|
811
|
-
},
|
|
812
|
-
'Inventory movement failed',
|
|
813
|
-
);
|
|
814
|
-
}
|
|
875
|
+
if (capacity.maxTargetOutput > 0n) {
|
|
876
|
+
viableSources.push({ chain: source.chain, ...capacity });
|
|
815
877
|
}
|
|
878
|
+
}
|
|
816
879
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
route,
|
|
822
|
-
success: false,
|
|
823
|
-
error: `All inventory movements failed${errorDetails}`,
|
|
824
|
-
};
|
|
825
|
-
}
|
|
880
|
+
// Sort by destination output descending.
|
|
881
|
+
viableSources.sort((a, b) =>
|
|
882
|
+
a.maxTargetOutput > b.maxTargetOutput ? -1 : 1,
|
|
883
|
+
);
|
|
826
884
|
|
|
827
|
-
|
|
885
|
+
if (viableSources.length === 0) {
|
|
886
|
+
this.logger.warn(
|
|
828
887
|
{
|
|
829
888
|
targetChain: destination,
|
|
830
|
-
|
|
831
|
-
totalBridged: totalBridged.toString(),
|
|
832
|
-
targetAmount: amount.toString(),
|
|
889
|
+
sourcesChecked: allSources.length,
|
|
833
890
|
intentId: intent.id,
|
|
834
891
|
},
|
|
835
|
-
'
|
|
892
|
+
'No viable bridge sources - all chains have insufficient inventory or high gas costs',
|
|
836
893
|
);
|
|
837
894
|
|
|
838
|
-
return {
|
|
895
|
+
return {
|
|
896
|
+
route,
|
|
897
|
+
success: false,
|
|
898
|
+
error: 'No viable bridge sources available',
|
|
899
|
+
};
|
|
839
900
|
}
|
|
901
|
+
|
|
902
|
+
// Create bridge plans using destination-local output amounts.
|
|
903
|
+
const shortfall =
|
|
904
|
+
requestedLocalAmount > availableInventory
|
|
905
|
+
? requestedLocalAmount - availableInventory
|
|
906
|
+
: 0n;
|
|
907
|
+
const targetWithBuffer =
|
|
908
|
+
((shortfall + costs.totalCost) * (100n + BRIDGE_BUFFER_PERCENT)) / 100n;
|
|
909
|
+
const bridgePlans: Array<{
|
|
910
|
+
chain: ChainName;
|
|
911
|
+
maxSourceInput: bigint;
|
|
912
|
+
targetOutput: bigint;
|
|
913
|
+
}> = [];
|
|
914
|
+
let totalPlanned = 0n;
|
|
915
|
+
|
|
916
|
+
for (const source of viableSources) {
|
|
917
|
+
if (totalPlanned >= targetWithBuffer) break;
|
|
918
|
+
|
|
919
|
+
const remaining = targetWithBuffer - totalPlanned;
|
|
920
|
+
const targetOutput =
|
|
921
|
+
source.maxTargetOutput >= remaining
|
|
922
|
+
? remaining
|
|
923
|
+
: source.maxTargetOutput;
|
|
924
|
+
|
|
925
|
+
bridgePlans.push({
|
|
926
|
+
chain: source.chain,
|
|
927
|
+
maxSourceInput: source.maxSourceInput,
|
|
928
|
+
targetOutput,
|
|
929
|
+
});
|
|
930
|
+
totalPlanned += targetOutput;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
this.logger.info(
|
|
934
|
+
{
|
|
935
|
+
targetChain: destination,
|
|
936
|
+
viableSources: viableSources.map((s) => ({
|
|
937
|
+
chain: s.chain,
|
|
938
|
+
maxSourceInput: s.maxSourceInput.toString(),
|
|
939
|
+
maxTargetOutput: s.maxTargetOutput.toString(),
|
|
940
|
+
})),
|
|
941
|
+
bridgePlans: bridgePlans.map((p) => ({
|
|
942
|
+
chain: p.chain,
|
|
943
|
+
maxSourceInput: p.maxSourceInput.toString(),
|
|
944
|
+
targetOutput: p.targetOutput.toString(),
|
|
945
|
+
})),
|
|
946
|
+
totalPlanned: totalPlanned.toString(),
|
|
947
|
+
shortfall: shortfall.toString(),
|
|
948
|
+
targetWithBuffer: targetWithBuffer.toString(),
|
|
949
|
+
intentId: intent.id,
|
|
950
|
+
},
|
|
951
|
+
'Created bridge plans using gas-adjusted viable amounts',
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
// Execute all bridges in parallel
|
|
955
|
+
const bridgeResults = await Promise.allSettled(
|
|
956
|
+
bridgePlans.map((plan) =>
|
|
957
|
+
this.executeInventoryMovement(
|
|
958
|
+
plan.chain,
|
|
959
|
+
destination,
|
|
960
|
+
plan.targetOutput,
|
|
961
|
+
plan.maxSourceInput,
|
|
962
|
+
intent,
|
|
963
|
+
route.externalBridge,
|
|
964
|
+
),
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
// Process results
|
|
969
|
+
let successCount = 0;
|
|
970
|
+
let totalBridged = 0n;
|
|
971
|
+
const failedErrors: string[] = [];
|
|
972
|
+
|
|
973
|
+
for (let i = 0; i < bridgeResults.length; i++) {
|
|
974
|
+
const result = bridgeResults[i];
|
|
975
|
+
const plan = bridgePlans[i];
|
|
976
|
+
|
|
977
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
978
|
+
successCount++;
|
|
979
|
+
totalBridged += plan.targetOutput;
|
|
980
|
+
this.logger.info(
|
|
981
|
+
{
|
|
982
|
+
sourceChain: plan.chain,
|
|
983
|
+
amount: plan.targetOutput.toString(),
|
|
984
|
+
txHash: result.value.txHash,
|
|
985
|
+
},
|
|
986
|
+
'Inventory movement succeeded',
|
|
987
|
+
);
|
|
988
|
+
} else {
|
|
989
|
+
const error =
|
|
990
|
+
result.status === 'rejected'
|
|
991
|
+
? result.reason?.message
|
|
992
|
+
: result.value.error;
|
|
993
|
+
if (error) {
|
|
994
|
+
failedErrors.push(`${plan.chain}: ${error}`);
|
|
995
|
+
}
|
|
996
|
+
this.logger.warn(
|
|
997
|
+
{
|
|
998
|
+
sourceChain: plan.chain,
|
|
999
|
+
amount: plan.targetOutput.toString(),
|
|
1000
|
+
error,
|
|
1001
|
+
},
|
|
1002
|
+
'Inventory movement failed',
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (successCount === 0) {
|
|
1008
|
+
const errorDetails =
|
|
1009
|
+
failedErrors.length > 0 ? ` (${failedErrors.join('; ')})` : '';
|
|
1010
|
+
return {
|
|
1011
|
+
route,
|
|
1012
|
+
success: false,
|
|
1013
|
+
error: `All inventory movements failed${errorDetails}`,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this.logger.info(
|
|
1018
|
+
{
|
|
1019
|
+
targetChain: destination,
|
|
1020
|
+
successCount,
|
|
1021
|
+
totalBridged: totalBridged.toString(),
|
|
1022
|
+
targetAmount: requestedLocalAmount.toString(),
|
|
1023
|
+
targetAmountCanonical: amount.toString(),
|
|
1024
|
+
intentId: intent.id,
|
|
1025
|
+
},
|
|
1026
|
+
'Parallel inventory movements completed, transferRemote will execute after bridges complete',
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
return { route, success: true };
|
|
840
1030
|
}
|
|
841
1031
|
|
|
842
1032
|
/**
|
|
@@ -852,12 +1042,11 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
852
1042
|
*
|
|
853
1043
|
* @param route - The transfer route (swapped direction)
|
|
854
1044
|
* @param intent - The rebalance intent being executed
|
|
855
|
-
* @param gasQuote - Pre-calculated gas quote from calculateTransferCosts
|
|
856
1045
|
*/
|
|
857
1046
|
private async executeTransferRemote(
|
|
858
1047
|
route: InventoryRoute,
|
|
859
1048
|
intent: RebalanceIntent,
|
|
860
|
-
|
|
1049
|
+
fulfilledCanonicalAmount: bigint,
|
|
861
1050
|
): Promise<InventoryExecutionResult> {
|
|
862
1051
|
const { origin, destination, amount } = route;
|
|
863
1052
|
|
|
@@ -869,52 +1058,16 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
869
1058
|
const destinationDomain = this.multiProvider.getDomainId(destination);
|
|
870
1059
|
|
|
871
1060
|
this.logger.debug(
|
|
872
|
-
{
|
|
873
|
-
|
|
874
|
-
destination,
|
|
875
|
-
amount: amount.toString(),
|
|
876
|
-
gasQuote: {
|
|
877
|
-
igpQuote: gasQuote.igpQuote.amount.toString(),
|
|
878
|
-
tokenFeeQuote: gasQuote.tokenFeeQuote?.amount?.toString() ?? 'none',
|
|
879
|
-
},
|
|
880
|
-
},
|
|
881
|
-
'Using pre-calculated gas quote for transferRemote',
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
// Convert pre-calculated gas quote to TokenAmount for WarpCore
|
|
885
|
-
const originChainMetadata = this.multiProvider.getChainMetadata(origin);
|
|
886
|
-
const igpAddressOrDenom = gasQuote.igpQuote.addressOrDenom;
|
|
887
|
-
const igpToken =
|
|
888
|
-
!igpAddressOrDenom || isZeroishAddress(igpAddressOrDenom)
|
|
889
|
-
? Token.FromChainMetadataNativeToken(originChainMetadata)
|
|
890
|
-
: this.warpCore.findToken(origin, igpAddressOrDenom);
|
|
891
|
-
assert(igpToken, `IGP fee token ${igpAddressOrDenom} is unknown`);
|
|
892
|
-
const interchainFee = new TokenAmount<IToken>(
|
|
893
|
-
gasQuote.igpQuote.amount,
|
|
894
|
-
igpToken,
|
|
1061
|
+
{ origin, destination, amount: amount.toString() },
|
|
1062
|
+
'Building transferRemote transactions for exact execution amount',
|
|
895
1063
|
);
|
|
896
1064
|
|
|
897
|
-
let tokenFeeQuote: TokenAmount<IToken> | undefined;
|
|
898
|
-
if (gasQuote.tokenFeeQuote?.amount) {
|
|
899
|
-
const feeAddress = gasQuote.tokenFeeQuote.addressOrDenom;
|
|
900
|
-
const feeToken =
|
|
901
|
-
!feeAddress || isZeroishAddress(feeAddress)
|
|
902
|
-
? Token.FromChainMetadataNativeToken(originChainMetadata)
|
|
903
|
-
: originToken;
|
|
904
|
-
tokenFeeQuote = new TokenAmount<IToken>(
|
|
905
|
-
gasQuote.tokenFeeQuote.amount,
|
|
906
|
-
feeToken,
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
1065
|
const originTokenAmount = originToken.amount(amount);
|
|
911
1066
|
const transferTxs = await this.warpCore.getTransferRemoteTxs({
|
|
912
1067
|
originTokenAmount,
|
|
913
1068
|
destination,
|
|
914
1069
|
sender: this.getInventorySignerAddress(origin),
|
|
915
1070
|
recipient: this.getInventorySignerAddress(destination),
|
|
916
|
-
interchainFee,
|
|
917
|
-
tokenFeeQuote,
|
|
918
1071
|
});
|
|
919
1072
|
assert(
|
|
920
1073
|
transferTxs.length > 0,
|
|
@@ -974,7 +1127,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
974
1127
|
intentId: intent.id,
|
|
975
1128
|
origin: this.multiProvider.getDomainId(origin),
|
|
976
1129
|
destination: destinationDomain,
|
|
977
|
-
amount,
|
|
1130
|
+
amount: fulfilledCanonicalAmount,
|
|
978
1131
|
type: 'inventory_deposit',
|
|
979
1132
|
txHash: transferTxHash,
|
|
980
1133
|
messageId,
|
|
@@ -1129,40 +1282,30 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1129
1282
|
}
|
|
1130
1283
|
|
|
1131
1284
|
/**
|
|
1132
|
-
* Calculate the
|
|
1133
|
-
* Uses LiFi
|
|
1134
|
-
*
|
|
1285
|
+
* Calculate the bridge capacity from a source chain in destination-local units.
|
|
1286
|
+
* Uses LiFi quotes to conservatively estimate the destination output available
|
|
1287
|
+
* from the source chain's current local inventory.
|
|
1135
1288
|
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
* - Applies conservative 20x buffer (LiFi underestimates by ~14x historically)
|
|
1139
|
-
* - Returns 0 if gas > 10% of inventory (not worth bridging)
|
|
1140
|
-
* - Returns inventory - estimatedGas if viable
|
|
1141
|
-
*
|
|
1142
|
-
* @param sourceChain - Chain to bridge from
|
|
1143
|
-
* @param targetChain - Chain to bridge to
|
|
1144
|
-
* @param rawInventory - Raw available inventory on source chain
|
|
1145
|
-
* @param externalBridgeType - External bridge type to use
|
|
1146
|
-
* @returns Maximum viable bridge amount (0 if not viable)
|
|
1289
|
+
* For native-token sources, gas is reserved from the source inventory and the
|
|
1290
|
+
* output capacity is re-quoted from the remaining source input.
|
|
1147
1291
|
*/
|
|
1148
|
-
private async
|
|
1292
|
+
private async calculateBridgeCapacity(
|
|
1149
1293
|
sourceChain: ChainName,
|
|
1150
1294
|
targetChain: ChainName,
|
|
1151
1295
|
rawInventory: bigint,
|
|
1152
1296
|
externalBridgeType: ExternalBridgeType,
|
|
1153
|
-
): Promise<
|
|
1297
|
+
): Promise<BridgeCapacity> {
|
|
1154
1298
|
const sourceToken = this.getTokenForChain(sourceChain);
|
|
1155
1299
|
const targetToken = this.getTokenForChain(targetChain);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
// Only applies to native tokens (need gas from same balance)
|
|
1160
|
-
if (!isNativeTokenStandard(sourceToken.standard)) {
|
|
1161
|
-
return rawInventory; // ERC20s don't compete with gas
|
|
1162
|
-
}
|
|
1300
|
+
assert(sourceToken, `No token found for source chain: ${sourceChain}`);
|
|
1301
|
+
assert(targetToken, `No token found for target chain: ${targetChain}`);
|
|
1163
1302
|
|
|
1164
1303
|
// Convert HypNative token addresses to the external bridge's native token representation
|
|
1165
|
-
const fromTokenAddress =
|
|
1304
|
+
const fromTokenAddress = getExternalBridgeTokenAddress(
|
|
1305
|
+
sourceToken,
|
|
1306
|
+
externalBridgeType,
|
|
1307
|
+
this.getNativeTokenAddress.bind(this),
|
|
1308
|
+
);
|
|
1166
1309
|
const toTokenAddress = getExternalBridgeTokenAddress(
|
|
1167
1310
|
targetToken,
|
|
1168
1311
|
externalBridgeType,
|
|
@@ -1174,7 +1317,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1174
1317
|
|
|
1175
1318
|
try {
|
|
1176
1319
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
1177
|
-
const
|
|
1320
|
+
const initialQuote = await externalBridge.quote({
|
|
1178
1321
|
fromChain: sourceChainId,
|
|
1179
1322
|
toChain: targetChainId,
|
|
1180
1323
|
fromToken: fromTokenAddress,
|
|
@@ -1184,48 +1327,58 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1184
1327
|
toAddress: this.getInventorySignerAddress(targetChain),
|
|
1185
1328
|
});
|
|
1186
1329
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1330
|
+
let maxSourceInput = rawInventory;
|
|
1331
|
+
let outputQuote = initialQuote;
|
|
1189
1332
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
return 0n;
|
|
1208
|
-
}
|
|
1333
|
+
if (isNativeTokenStandard(sourceToken.standard)) {
|
|
1334
|
+
const estimatedGas = initialQuote.gasCosts * GAS_COST_MULTIPLIER;
|
|
1335
|
+
const maxGasThreshold = rawInventory / MAX_GAS_PERCENT_THRESHOLD;
|
|
1336
|
+
if (estimatedGas > maxGasThreshold) {
|
|
1337
|
+
this.logger.info(
|
|
1338
|
+
{
|
|
1339
|
+
sourceChain,
|
|
1340
|
+
targetChain,
|
|
1341
|
+
rawInventory: rawInventory.toString(),
|
|
1342
|
+
quotedGas: initialQuote.gasCosts.toString(),
|
|
1343
|
+
estimatedGas: estimatedGas.toString(),
|
|
1344
|
+
maxGasThreshold: maxGasThreshold.toString(),
|
|
1345
|
+
},
|
|
1346
|
+
'Bridge not viable - gas cost exceeds 10% of inventory',
|
|
1347
|
+
);
|
|
1348
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
1349
|
+
}
|
|
1209
1350
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1351
|
+
maxSourceInput = rawInventory - estimatedGas;
|
|
1352
|
+
if (maxSourceInput <= 0n) {
|
|
1353
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
outputQuote = await externalBridge.quote({
|
|
1357
|
+
fromChain: sourceChainId,
|
|
1358
|
+
toChain: targetChainId,
|
|
1359
|
+
fromToken: fromTokenAddress,
|
|
1360
|
+
toToken: toTokenAddress,
|
|
1361
|
+
fromAmount: maxSourceInput,
|
|
1362
|
+
fromAddress: this.getInventorySignerAddress(sourceChain),
|
|
1363
|
+
toAddress: this.getInventorySignerAddress(targetChain),
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1212
1366
|
|
|
1213
1367
|
this.logger.info(
|
|
1214
1368
|
{
|
|
1215
1369
|
sourceChain,
|
|
1216
1370
|
targetChain,
|
|
1217
1371
|
rawInventory: rawInventory.toString(),
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
estimatedGas: estimatedGas.toString(),
|
|
1221
|
-
estimatedGasEth: (Number(estimatedGas) / 1e18).toFixed(6),
|
|
1222
|
-
maxViable: maxViable.toString(),
|
|
1223
|
-
maxViableEth: (Number(maxViable) / 1e18).toFixed(6),
|
|
1372
|
+
maxSourceInput: maxSourceInput.toString(),
|
|
1373
|
+
maxTargetOutput: outputQuote.toAmountMin.toString(),
|
|
1224
1374
|
},
|
|
1225
|
-
'Calculated
|
|
1375
|
+
'Calculated bridge capacity',
|
|
1226
1376
|
);
|
|
1227
1377
|
|
|
1228
|
-
return
|
|
1378
|
+
return {
|
|
1379
|
+
maxSourceInput,
|
|
1380
|
+
maxTargetOutput: outputQuote.toAmountMin,
|
|
1381
|
+
};
|
|
1229
1382
|
} catch (error) {
|
|
1230
1383
|
this.logger.warn(
|
|
1231
1384
|
{
|
|
@@ -1233,21 +1386,22 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1233
1386
|
targetChain,
|
|
1234
1387
|
error: (error as Error).message,
|
|
1235
1388
|
},
|
|
1236
|
-
'Failed to calculate
|
|
1389
|
+
'Failed to calculate bridge capacity, skipping chain',
|
|
1237
1390
|
);
|
|
1238
|
-
return 0n;
|
|
1391
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
1239
1392
|
}
|
|
1240
1393
|
}
|
|
1241
1394
|
|
|
1242
1395
|
/**
|
|
1243
1396
|
* Execute inventory movement from source chain to target chain via LiFi bridge.
|
|
1244
1397
|
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1398
|
+
* Uses reverse quotes (`toAmount`) so plans are expressed in target-chain local
|
|
1399
|
+
* units and source-local spend is discovered by the bridge quote.
|
|
1247
1400
|
*
|
|
1248
1401
|
* @param sourceChain - Chain to move inventory from
|
|
1249
1402
|
* @param targetChain - Chain to move inventory to (origin chain for rebalancing)
|
|
1250
|
-
* @param
|
|
1403
|
+
* @param targetOutputAmount - Destination-local amount to receive
|
|
1404
|
+
* @param maxSourceInput - Maximum source-local amount available for this plan
|
|
1251
1405
|
* @param intent - Rebalance intent for tracking
|
|
1252
1406
|
* @param externalBridgeType - External bridge type to use
|
|
1253
1407
|
* @returns Result with success status and optional txHash/error
|
|
@@ -1255,7 +1409,8 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1255
1409
|
private async executeInventoryMovement(
|
|
1256
1410
|
sourceChain: ChainName,
|
|
1257
1411
|
targetChain: ChainName,
|
|
1258
|
-
|
|
1412
|
+
targetOutputAmount: bigint,
|
|
1413
|
+
maxSourceInput: bigint,
|
|
1259
1414
|
intent: RebalanceIntent,
|
|
1260
1415
|
externalBridgeType: ExternalBridgeType,
|
|
1261
1416
|
): Promise<{ success: boolean; txHash?: string; error?: string }> {
|
|
@@ -1304,44 +1459,6 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1304
1459
|
'Resolved token addresses for LiFi bridge',
|
|
1305
1460
|
);
|
|
1306
1461
|
|
|
1307
|
-
// Calculate minViableTransfer for the target chain
|
|
1308
|
-
// If bridging less than this, the received amount won't be enough to execute transferRemote
|
|
1309
|
-
// So we over-bridge to ensure we can complete the intent in the next cycle
|
|
1310
|
-
const costs = await calculateTransferCosts(
|
|
1311
|
-
targetChain, // FROM chain for transferRemote (the target of this bridge)
|
|
1312
|
-
sourceChain, // TO chain for transferRemote (Hyperlane message destination)
|
|
1313
|
-
amount, // availableInventory (not used for minViableTransfer calculation)
|
|
1314
|
-
amount, // requestedAmount
|
|
1315
|
-
this.multiProvider,
|
|
1316
|
-
this.warpCore.multiProvider,
|
|
1317
|
-
this.getTokenForChain.bind(this),
|
|
1318
|
-
this.getInventorySignerAddress(targetChain),
|
|
1319
|
-
isNativeTokenStandard,
|
|
1320
|
-
this.logger,
|
|
1321
|
-
);
|
|
1322
|
-
const { minViableTransfer } = costs;
|
|
1323
|
-
|
|
1324
|
-
// If the requested amount is below minViableTransfer, adjust it up
|
|
1325
|
-
// This ensures we bridge enough to actually complete the final transferRemote
|
|
1326
|
-
const effectiveAmount =
|
|
1327
|
-
amount < minViableTransfer ? minViableTransfer : amount;
|
|
1328
|
-
|
|
1329
|
-
if (effectiveAmount !== amount) {
|
|
1330
|
-
this.logger.info(
|
|
1331
|
-
{
|
|
1332
|
-
originalAmount: amount.toString(),
|
|
1333
|
-
effectiveAmount: effectiveAmount.toString(),
|
|
1334
|
-
minViableTransfer: minViableTransfer.toString(),
|
|
1335
|
-
originalAmountEth: (Number(amount) / 1e18).toFixed(6),
|
|
1336
|
-
effectiveAmountEth: (Number(effectiveAmount) / 1e18).toFixed(6),
|
|
1337
|
-
minViableTransferEth: (Number(minViableTransfer) / 1e18).toFixed(6),
|
|
1338
|
-
adjustedUp: true,
|
|
1339
|
-
intentId: intent.id,
|
|
1340
|
-
},
|
|
1341
|
-
'Over-bridging to minViableTransfer to ensure final transferRemote can complete',
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
1462
|
try {
|
|
1346
1463
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
1347
1464
|
const quote = await externalBridge.quote({
|
|
@@ -1349,12 +1466,18 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1349
1466
|
toChain: targetChainId,
|
|
1350
1467
|
fromToken: fromTokenAddress,
|
|
1351
1468
|
toToken: toTokenAddress,
|
|
1352
|
-
|
|
1469
|
+
toAmount: targetOutputAmount,
|
|
1353
1470
|
fromAddress: this.getInventorySignerAddress(sourceChain),
|
|
1354
1471
|
toAddress: this.getInventorySignerAddress(targetChain),
|
|
1355
1472
|
});
|
|
1356
1473
|
|
|
1357
1474
|
const inputRequired = quote.fromAmount;
|
|
1475
|
+
if (inputRequired > maxSourceInput) {
|
|
1476
|
+
return {
|
|
1477
|
+
success: false,
|
|
1478
|
+
error: `Bridge input ${inputRequired} exceeded planned source capacity ${maxSourceInput}`,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1358
1481
|
|
|
1359
1482
|
this.logger.info(
|
|
1360
1483
|
{
|
|
@@ -1362,19 +1485,27 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1362
1485
|
targetChain,
|
|
1363
1486
|
sourceChainId,
|
|
1364
1487
|
targetChainId,
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1488
|
+
requestedTargetOutput: targetOutputAmount.toString(),
|
|
1489
|
+
requestedTargetOutputFormatted: this.formatLocalAmount(
|
|
1490
|
+
targetOutputAmount,
|
|
1491
|
+
targetToken,
|
|
1492
|
+
),
|
|
1369
1493
|
inputRequired: inputRequired.toString(),
|
|
1494
|
+
inputRequiredFormatted: this.formatLocalAmount(
|
|
1495
|
+
inputRequired,
|
|
1496
|
+
sourceToken,
|
|
1497
|
+
),
|
|
1370
1498
|
expectedOutput: quote.toAmount.toString(),
|
|
1371
|
-
|
|
1499
|
+
expectedOutputMin: quote.toAmountMin.toString(),
|
|
1500
|
+
expectedOutputFormatted: this.formatLocalAmount(
|
|
1501
|
+
quote.toAmount,
|
|
1502
|
+
targetToken,
|
|
1503
|
+
),
|
|
1372
1504
|
gasCosts: quote.gasCosts.toString(),
|
|
1373
1505
|
feeCosts: quote.feeCosts.toString(),
|
|
1374
1506
|
intentId: intent.id,
|
|
1375
|
-
adjustedForMinViable: effectiveAmount > amount,
|
|
1376
1507
|
},
|
|
1377
|
-
'Executing inventory movement via LiFi
|
|
1508
|
+
'Executing inventory movement via LiFi reverse quote',
|
|
1378
1509
|
);
|
|
1379
1510
|
|
|
1380
1511
|
this.logger.debug(
|
|
@@ -1417,6 +1548,8 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1417
1548
|
'Inventory movement transaction executed',
|
|
1418
1549
|
);
|
|
1419
1550
|
|
|
1551
|
+
// Keep bridge consumption in source-local units; intent fulfillment only
|
|
1552
|
+
// advances from canonical inventory_deposit amounts after transferRemote.
|
|
1420
1553
|
await this.actionTracker.createRebalanceAction({
|
|
1421
1554
|
intentId: intent.id,
|
|
1422
1555
|
origin: this.multiProvider.getDomainId(sourceChain),
|
|
@@ -1446,7 +1579,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1446
1579
|
{
|
|
1447
1580
|
sourceChain,
|
|
1448
1581
|
targetChain,
|
|
1449
|
-
amount:
|
|
1582
|
+
amount: targetOutputAmount.toString(),
|
|
1450
1583
|
intentId: intent.id,
|
|
1451
1584
|
error: (error as Error).message,
|
|
1452
1585
|
},
|