@hyperlane-xyz/rebalancer 27.2.13 → 27.2.14
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/bridges/LiFiBridge.js +1 -1
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +37 -0
- package/dist/bridges/LiFiBridge.test.js.map +1 -1
- package/dist/core/InventoryRebalancer.d.ts +4 -2
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +81 -23
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +328 -20
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/package.json +7 -7
- package/src/bridges/LiFiBridge.test.ts +43 -0
- package/src/bridges/LiFiBridge.ts +1 -1
- package/src/core/InventoryRebalancer.test.ts +450 -21
- package/src/core/InventoryRebalancer.ts +113 -28
|
@@ -74,6 +74,22 @@ type BridgeCapacity = {
|
|
|
74
74
|
maxTargetOutput: bigint;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
type BridgeQuoteMode = 'forward' | 'reverse';
|
|
78
|
+
|
|
79
|
+
type InventoryMovementExecutionResult =
|
|
80
|
+
| {
|
|
81
|
+
success: true;
|
|
82
|
+
txHash: string;
|
|
83
|
+
inputRequired: bigint;
|
|
84
|
+
quotedOutput: bigint;
|
|
85
|
+
quotedOutputMin: bigint;
|
|
86
|
+
quoteModeUsed: BridgeQuoteMode;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
success: false;
|
|
90
|
+
error: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
77
93
|
const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
|
|
78
94
|
'balance may be insufficient',
|
|
79
95
|
'transfer amount exceeds balance',
|
|
@@ -910,6 +926,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
910
926
|
chain: ChainName;
|
|
911
927
|
maxSourceInput: bigint;
|
|
912
928
|
targetOutput: bigint;
|
|
929
|
+
quoteMode: BridgeQuoteMode;
|
|
913
930
|
}> = [];
|
|
914
931
|
let totalPlanned = 0n;
|
|
915
932
|
|
|
@@ -921,11 +938,14 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
921
938
|
source.maxTargetOutput >= remaining
|
|
922
939
|
? remaining
|
|
923
940
|
: source.maxTargetOutput;
|
|
941
|
+
const quoteMode: BridgeQuoteMode =
|
|
942
|
+
source.maxTargetOutput > remaining ? 'reverse' : 'forward';
|
|
924
943
|
|
|
925
944
|
bridgePlans.push({
|
|
926
945
|
chain: source.chain,
|
|
927
946
|
maxSourceInput: source.maxSourceInput,
|
|
928
947
|
targetOutput,
|
|
948
|
+
quoteMode,
|
|
929
949
|
});
|
|
930
950
|
totalPlanned += targetOutput;
|
|
931
951
|
}
|
|
@@ -942,6 +962,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
942
962
|
chain: p.chain,
|
|
943
963
|
maxSourceInput: p.maxSourceInput.toString(),
|
|
944
964
|
targetOutput: p.targetOutput.toString(),
|
|
965
|
+
quoteMode: p.quoteMode,
|
|
945
966
|
})),
|
|
946
967
|
totalPlanned: totalPlanned.toString(),
|
|
947
968
|
shortfall: shortfall.toString(),
|
|
@@ -959,6 +980,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
959
980
|
destination,
|
|
960
981
|
plan.targetOutput,
|
|
961
982
|
plan.maxSourceInput,
|
|
983
|
+
plan.quoteMode,
|
|
962
984
|
intent,
|
|
963
985
|
route.externalBridge,
|
|
964
986
|
),
|
|
@@ -967,7 +989,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
967
989
|
|
|
968
990
|
// Process results
|
|
969
991
|
let successCount = 0;
|
|
970
|
-
let
|
|
992
|
+
let totalQuotedOutputMin = 0n;
|
|
971
993
|
const failedErrors: string[] = [];
|
|
972
994
|
|
|
973
995
|
for (let i = 0; i < bridgeResults.length; i++) {
|
|
@@ -976,27 +998,42 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
976
998
|
|
|
977
999
|
if (result.status === 'fulfilled' && result.value.success) {
|
|
978
1000
|
successCount++;
|
|
979
|
-
|
|
1001
|
+
totalQuotedOutputMin += result.value.quotedOutputMin;
|
|
980
1002
|
this.logger.info(
|
|
981
1003
|
{
|
|
982
1004
|
sourceChain: plan.chain,
|
|
983
|
-
|
|
1005
|
+
plannedTargetOutput: plan.targetOutput.toString(),
|
|
1006
|
+
quotedOutput: result.value.quotedOutput.toString(),
|
|
1007
|
+
quotedOutputMin: result.value.quotedOutputMin.toString(),
|
|
1008
|
+
quoteModeUsed: result.value.quoteModeUsed,
|
|
984
1009
|
txHash: result.value.txHash,
|
|
985
1010
|
},
|
|
986
1011
|
'Inventory movement succeeded',
|
|
987
1012
|
);
|
|
988
1013
|
} else {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1014
|
+
let error: string | undefined;
|
|
1015
|
+
if (result.status === 'rejected') {
|
|
1016
|
+
if (result.reason instanceof Error) {
|
|
1017
|
+
error = result.reason.message;
|
|
1018
|
+
} else if (typeof result.reason === 'string') {
|
|
1019
|
+
error = result.reason;
|
|
1020
|
+
} else {
|
|
1021
|
+
try {
|
|
1022
|
+
error = JSON.stringify(result.reason) ?? String(result.reason);
|
|
1023
|
+
} catch {
|
|
1024
|
+
error = String(result.reason);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} else if (!result.value.success) {
|
|
1028
|
+
error = result.value.error;
|
|
1029
|
+
}
|
|
993
1030
|
if (error) {
|
|
994
1031
|
failedErrors.push(`${plan.chain}: ${error}`);
|
|
995
1032
|
}
|
|
996
1033
|
this.logger.warn(
|
|
997
1034
|
{
|
|
998
1035
|
sourceChain: plan.chain,
|
|
999
|
-
|
|
1036
|
+
plannedTargetOutput: plan.targetOutput.toString(),
|
|
1000
1037
|
error,
|
|
1001
1038
|
},
|
|
1002
1039
|
'Inventory movement failed',
|
|
@@ -1018,7 +1055,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1018
1055
|
{
|
|
1019
1056
|
targetChain: destination,
|
|
1020
1057
|
successCount,
|
|
1021
|
-
|
|
1058
|
+
totalQuotedOutputMin: totalQuotedOutputMin.toString(),
|
|
1022
1059
|
targetAmount: requestedLocalAmount.toString(),
|
|
1023
1060
|
targetAmountCanonical: amount.toString(),
|
|
1024
1061
|
intentId: intent.id,
|
|
@@ -1395,13 +1432,15 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1395
1432
|
/**
|
|
1396
1433
|
* Execute inventory movement from source chain to target chain via LiFi bridge.
|
|
1397
1434
|
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1435
|
+
* Quote mode is chosen during planning:
|
|
1436
|
+
* - `reverse`: request an exact target-chain output when the source has headroom
|
|
1437
|
+
* - `forward`: spend the source cap directly when source inventory is the limiter
|
|
1400
1438
|
*
|
|
1401
1439
|
* @param sourceChain - Chain to move inventory from
|
|
1402
1440
|
* @param targetChain - Chain to move inventory to (origin chain for rebalancing)
|
|
1403
1441
|
* @param targetOutputAmount - Destination-local amount to receive
|
|
1404
1442
|
* @param maxSourceInput - Maximum source-local amount available for this plan
|
|
1443
|
+
* @param quoteMode - Whether to execute this bridge plan as exact-input or exact-output
|
|
1405
1444
|
* @param intent - Rebalance intent for tracking
|
|
1406
1445
|
* @param externalBridgeType - External bridge type to use
|
|
1407
1446
|
* @returns Result with success status and optional txHash/error
|
|
@@ -1411,9 +1450,10 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1411
1450
|
targetChain: ChainName,
|
|
1412
1451
|
targetOutputAmount: bigint,
|
|
1413
1452
|
maxSourceInput: bigint,
|
|
1453
|
+
quoteMode: BridgeQuoteMode,
|
|
1414
1454
|
intent: RebalanceIntent,
|
|
1415
1455
|
externalBridgeType: ExternalBridgeType,
|
|
1416
|
-
): Promise<
|
|
1456
|
+
): Promise<InventoryMovementExecutionResult> {
|
|
1417
1457
|
const sourceToken = this.getTokenForChain(sourceChain);
|
|
1418
1458
|
if (!sourceToken) {
|
|
1419
1459
|
return {
|
|
@@ -1461,15 +1501,43 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1461
1501
|
|
|
1462
1502
|
try {
|
|
1463
1503
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1504
|
+
const fromAddress = this.getInventorySignerAddress(sourceChain);
|
|
1505
|
+
const toAddress = this.getInventorySignerAddress(targetChain);
|
|
1506
|
+
const quoteWithMode = async (mode: BridgeQuoteMode) =>
|
|
1507
|
+
externalBridge.quote({
|
|
1508
|
+
fromChain: sourceChainId,
|
|
1509
|
+
toChain: targetChainId,
|
|
1510
|
+
fromToken: fromTokenAddress,
|
|
1511
|
+
toToken: toTokenAddress,
|
|
1512
|
+
...(mode === 'forward'
|
|
1513
|
+
? { fromAmount: maxSourceInput }
|
|
1514
|
+
: { toAmount: targetOutputAmount }),
|
|
1515
|
+
fromAddress,
|
|
1516
|
+
toAddress,
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
let quoteModeUsed = quoteMode;
|
|
1520
|
+
let quote = await quoteWithMode(quoteModeUsed);
|
|
1521
|
+
|
|
1522
|
+
if (quoteModeUsed === 'reverse' && quote.fromAmount > maxSourceInput) {
|
|
1523
|
+
this.logger.warn(
|
|
1524
|
+
{
|
|
1525
|
+
sourceChain,
|
|
1526
|
+
targetChain,
|
|
1527
|
+
plannedQuoteMode: quoteMode,
|
|
1528
|
+
requestedTargetOutput: targetOutputAmount.toString(),
|
|
1529
|
+
quotedInput: quote.fromAmount.toString(),
|
|
1530
|
+
maxSourceInput: maxSourceInput.toString(),
|
|
1531
|
+
intentId: intent.id,
|
|
1532
|
+
},
|
|
1533
|
+
'Reverse bridge quote exceeded source capacity, retrying with forward quote',
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
// Spend the full source cap on fallback; minor output drift is acceptable
|
|
1537
|
+
// and will be reconciled by later cycles rather than risking livelock.
|
|
1538
|
+
quoteModeUsed = 'forward';
|
|
1539
|
+
quote = await quoteWithMode(quoteModeUsed);
|
|
1540
|
+
}
|
|
1473
1541
|
|
|
1474
1542
|
const inputRequired = quote.fromAmount;
|
|
1475
1543
|
if (inputRequired > maxSourceInput) {
|
|
@@ -1490,22 +1558,30 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1490
1558
|
targetOutputAmount,
|
|
1491
1559
|
targetToken,
|
|
1492
1560
|
),
|
|
1561
|
+
quoteModePlanned: quoteMode,
|
|
1562
|
+
quoteModeUsed,
|
|
1563
|
+
retriedAsForward:
|
|
1564
|
+
quoteMode === 'reverse' && quoteModeUsed === 'forward',
|
|
1493
1565
|
inputRequired: inputRequired.toString(),
|
|
1494
1566
|
inputRequiredFormatted: this.formatLocalAmount(
|
|
1495
1567
|
inputRequired,
|
|
1496
1568
|
sourceToken,
|
|
1497
1569
|
),
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1570
|
+
quotedOutput: quote.toAmount.toString(),
|
|
1571
|
+
quotedOutputMin: quote.toAmountMin.toString(),
|
|
1572
|
+
quotedOutputFormatted: this.formatLocalAmount(
|
|
1501
1573
|
quote.toAmount,
|
|
1502
1574
|
targetToken,
|
|
1503
1575
|
),
|
|
1576
|
+
quotedOutputMinFormatted: this.formatLocalAmount(
|
|
1577
|
+
quote.toAmountMin,
|
|
1578
|
+
targetToken,
|
|
1579
|
+
),
|
|
1504
1580
|
gasCosts: quote.gasCosts.toString(),
|
|
1505
1581
|
feeCosts: quote.feeCosts.toString(),
|
|
1506
1582
|
intentId: intent.id,
|
|
1507
1583
|
},
|
|
1508
|
-
'Executing inventory movement via
|
|
1584
|
+
'Executing inventory movement via bridge quote',
|
|
1509
1585
|
);
|
|
1510
1586
|
|
|
1511
1587
|
this.logger.debug(
|
|
@@ -1573,22 +1649,31 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1573
1649
|
'Updated consumed inventory after LiFi bridge',
|
|
1574
1650
|
);
|
|
1575
1651
|
|
|
1576
|
-
return {
|
|
1652
|
+
return {
|
|
1653
|
+
success: true,
|
|
1654
|
+
txHash: result.txHash,
|
|
1655
|
+
inputRequired,
|
|
1656
|
+
quotedOutput: quote.toAmount,
|
|
1657
|
+
quotedOutputMin: quote.toAmountMin,
|
|
1658
|
+
quoteModeUsed,
|
|
1659
|
+
};
|
|
1577
1660
|
} catch (error) {
|
|
1661
|
+
const errorMessage =
|
|
1662
|
+
error instanceof Error ? error.message : String(error);
|
|
1578
1663
|
this.logger.error(
|
|
1579
1664
|
{
|
|
1580
1665
|
sourceChain,
|
|
1581
1666
|
targetChain,
|
|
1582
1667
|
amount: targetOutputAmount.toString(),
|
|
1583
1668
|
intentId: intent.id,
|
|
1584
|
-
error:
|
|
1669
|
+
error: errorMessage,
|
|
1585
1670
|
},
|
|
1586
1671
|
'Failed to execute inventory movement',
|
|
1587
1672
|
);
|
|
1588
1673
|
|
|
1589
1674
|
return {
|
|
1590
1675
|
success: false,
|
|
1591
|
-
error:
|
|
1676
|
+
error: errorMessage,
|
|
1592
1677
|
};
|
|
1593
1678
|
}
|
|
1594
1679
|
}
|