@hyperlane-xyz/rebalancer 27.2.13 → 27.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridges/LiFiBridge.d.ts +3 -3
- package/dist/bridges/LiFiBridge.d.ts.map +1 -1
- package/dist/bridges/LiFiBridge.js +60 -52
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +213 -0
- package/dist/bridges/LiFiBridge.test.js.map +1 -1
- package/dist/config/RebalancerConfig.test.js +123 -0
- package/dist/config/RebalancerConfig.test.js.map +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +9 -0
- package/dist/config/types.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 +84 -25
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +436 -21
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +34 -24
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.test.js +84 -1
- package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
- package/dist/service.js +7 -4
- package/dist/service.js.map +1 -1
- package/dist/utils/blockTag.d.ts.map +1 -1
- package/dist/utils/blockTag.js +8 -3
- package/dist/utils/blockTag.js.map +1 -1
- package/dist/utils/blockTag.test.d.ts +2 -0
- package/dist/utils/blockTag.test.d.ts.map +1 -0
- package/dist/utils/blockTag.test.js +57 -0
- package/dist/utils/blockTag.test.js.map +1 -0
- package/dist/utils/gasEstimation.js +4 -4
- package/dist/utils/gasEstimation.js.map +1 -1
- package/dist/utils/gasEstimation.test.d.ts +2 -0
- package/dist/utils/gasEstimation.test.d.ts.map +1 -0
- package/dist/utils/gasEstimation.test.js +63 -0
- package/dist/utils/gasEstimation.test.js.map +1 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/dist/utils/tokenUtils.js +5 -2
- package/dist/utils/tokenUtils.js.map +1 -1
- package/package.json +7 -7
- package/src/bridges/LiFiBridge.test.ts +270 -0
- package/src/bridges/LiFiBridge.ts +83 -68
- package/src/config/RebalancerConfig.test.ts +135 -0
- package/src/config/types.ts +8 -0
- package/src/core/InventoryRebalancer.test.ts +610 -21
- package/src/core/InventoryRebalancer.ts +121 -30
- package/src/factories/RebalancerContextFactory.test.ts +116 -1
- package/src/factories/RebalancerContextFactory.ts +38 -28
- package/src/service.ts +11 -8
- package/src/utils/blockTag.test.ts +70 -0
- package/src/utils/blockTag.ts +11 -3
- package/src/utils/gasEstimation.test.ts +99 -0
- package/src/utils/gasEstimation.ts +4 -4
- package/src/utils/tokenUtils.ts +5 -2
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
getSignerForChain,
|
|
16
16
|
type TypedTransactionReceipt,
|
|
17
17
|
} from '@hyperlane-xyz/sdk';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ProtocolType,
|
|
20
|
+
assert,
|
|
21
|
+
ensure0x,
|
|
22
|
+
fromWei,
|
|
23
|
+
isEVMLike,
|
|
24
|
+
} from '@hyperlane-xyz/utils';
|
|
19
25
|
|
|
20
26
|
import type { ExternalBridgeType } from '../config/types.js';
|
|
21
27
|
import type {
|
|
@@ -74,6 +80,21 @@ type BridgeCapacity = {
|
|
|
74
80
|
maxTargetOutput: bigint;
|
|
75
81
|
};
|
|
76
82
|
|
|
83
|
+
type BridgeQuoteMode = 'forward' | 'reverse';
|
|
84
|
+
|
|
85
|
+
type InventoryMovementExecutionResult =
|
|
86
|
+
| {
|
|
87
|
+
success: true;
|
|
88
|
+
txHash: string;
|
|
89
|
+
inputRequired: bigint;
|
|
90
|
+
quotedOutput: bigint;
|
|
91
|
+
quotedOutputMin: bigint;
|
|
92
|
+
quoteModeUsed: BridgeQuoteMode;
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
success: false;
|
|
96
|
+
error: string;
|
|
97
|
+
};
|
|
77
98
|
const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
|
|
78
99
|
'balance may be insufficient',
|
|
79
100
|
'transfer amount exceeds balance',
|
|
@@ -910,6 +931,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
910
931
|
chain: ChainName;
|
|
911
932
|
maxSourceInput: bigint;
|
|
912
933
|
targetOutput: bigint;
|
|
934
|
+
quoteMode: BridgeQuoteMode;
|
|
913
935
|
}> = [];
|
|
914
936
|
let totalPlanned = 0n;
|
|
915
937
|
|
|
@@ -921,11 +943,14 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
921
943
|
source.maxTargetOutput >= remaining
|
|
922
944
|
? remaining
|
|
923
945
|
: source.maxTargetOutput;
|
|
946
|
+
const quoteMode: BridgeQuoteMode =
|
|
947
|
+
source.maxTargetOutput > remaining ? 'reverse' : 'forward';
|
|
924
948
|
|
|
925
949
|
bridgePlans.push({
|
|
926
950
|
chain: source.chain,
|
|
927
951
|
maxSourceInput: source.maxSourceInput,
|
|
928
952
|
targetOutput,
|
|
953
|
+
quoteMode,
|
|
929
954
|
});
|
|
930
955
|
totalPlanned += targetOutput;
|
|
931
956
|
}
|
|
@@ -942,6 +967,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
942
967
|
chain: p.chain,
|
|
943
968
|
maxSourceInput: p.maxSourceInput.toString(),
|
|
944
969
|
targetOutput: p.targetOutput.toString(),
|
|
970
|
+
quoteMode: p.quoteMode,
|
|
945
971
|
})),
|
|
946
972
|
totalPlanned: totalPlanned.toString(),
|
|
947
973
|
shortfall: shortfall.toString(),
|
|
@@ -959,6 +985,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
959
985
|
destination,
|
|
960
986
|
plan.targetOutput,
|
|
961
987
|
plan.maxSourceInput,
|
|
988
|
+
plan.quoteMode,
|
|
962
989
|
intent,
|
|
963
990
|
route.externalBridge,
|
|
964
991
|
),
|
|
@@ -967,7 +994,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
967
994
|
|
|
968
995
|
// Process results
|
|
969
996
|
let successCount = 0;
|
|
970
|
-
let
|
|
997
|
+
let totalQuotedOutputMin = 0n;
|
|
971
998
|
const failedErrors: string[] = [];
|
|
972
999
|
|
|
973
1000
|
for (let i = 0; i < bridgeResults.length; i++) {
|
|
@@ -976,27 +1003,42 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
976
1003
|
|
|
977
1004
|
if (result.status === 'fulfilled' && result.value.success) {
|
|
978
1005
|
successCount++;
|
|
979
|
-
|
|
1006
|
+
totalQuotedOutputMin += result.value.quotedOutputMin;
|
|
980
1007
|
this.logger.info(
|
|
981
1008
|
{
|
|
982
1009
|
sourceChain: plan.chain,
|
|
983
|
-
|
|
1010
|
+
plannedTargetOutput: plan.targetOutput.toString(),
|
|
1011
|
+
quotedOutput: result.value.quotedOutput.toString(),
|
|
1012
|
+
quotedOutputMin: result.value.quotedOutputMin.toString(),
|
|
1013
|
+
quoteModeUsed: result.value.quoteModeUsed,
|
|
984
1014
|
txHash: result.value.txHash,
|
|
985
1015
|
},
|
|
986
1016
|
'Inventory movement succeeded',
|
|
987
1017
|
);
|
|
988
1018
|
} else {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1019
|
+
let error: string | undefined;
|
|
1020
|
+
if (result.status === 'rejected') {
|
|
1021
|
+
if (result.reason instanceof Error) {
|
|
1022
|
+
error = result.reason.message;
|
|
1023
|
+
} else if (typeof result.reason === 'string') {
|
|
1024
|
+
error = result.reason;
|
|
1025
|
+
} else {
|
|
1026
|
+
try {
|
|
1027
|
+
error = JSON.stringify(result.reason) ?? String(result.reason);
|
|
1028
|
+
} catch {
|
|
1029
|
+
error = String(result.reason);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} else if (!result.value.success) {
|
|
1033
|
+
error = result.value.error;
|
|
1034
|
+
}
|
|
993
1035
|
if (error) {
|
|
994
1036
|
failedErrors.push(`${plan.chain}: ${error}`);
|
|
995
1037
|
}
|
|
996
1038
|
this.logger.warn(
|
|
997
1039
|
{
|
|
998
1040
|
sourceChain: plan.chain,
|
|
999
|
-
|
|
1041
|
+
plannedTargetOutput: plan.targetOutput.toString(),
|
|
1000
1042
|
error,
|
|
1001
1043
|
},
|
|
1002
1044
|
'Inventory movement failed',
|
|
@@ -1018,7 +1060,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1018
1060
|
{
|
|
1019
1061
|
targetChain: destination,
|
|
1020
1062
|
successCount,
|
|
1021
|
-
|
|
1063
|
+
totalQuotedOutputMin: totalQuotedOutputMin.toString(),
|
|
1022
1064
|
targetAmount: requestedLocalAmount.toString(),
|
|
1023
1065
|
targetAmountCanonical: amount.toString(),
|
|
1024
1066
|
intentId: intent.id,
|
|
@@ -1184,6 +1226,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1184
1226
|
): MultiProtocolSignerSignerAccountInfo {
|
|
1185
1227
|
void chain;
|
|
1186
1228
|
switch (protocol) {
|
|
1229
|
+
case ProtocolType.Tron:
|
|
1187
1230
|
case ProtocolType.Ethereum:
|
|
1188
1231
|
return { protocol, privateKey: ensure0x(key) };
|
|
1189
1232
|
case ProtocolType.Sealevel:
|
|
@@ -1223,7 +1266,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1223
1266
|
try {
|
|
1224
1267
|
const protocol = this.getProtocolForChain(origin);
|
|
1225
1268
|
|
|
1226
|
-
if (protocol
|
|
1269
|
+
if (isEVMLike(protocol)) {
|
|
1227
1270
|
const provider =
|
|
1228
1271
|
this.warpCore.multiProvider.getEthersV5Provider(origin);
|
|
1229
1272
|
const receipt = await provider.getTransactionReceipt(txHash);
|
|
@@ -1395,13 +1438,15 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1395
1438
|
/**
|
|
1396
1439
|
* Execute inventory movement from source chain to target chain via LiFi bridge.
|
|
1397
1440
|
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1441
|
+
* Quote mode is chosen during planning:
|
|
1442
|
+
* - `reverse`: request an exact target-chain output when the source has headroom
|
|
1443
|
+
* - `forward`: spend the source cap directly when source inventory is the limiter
|
|
1400
1444
|
*
|
|
1401
1445
|
* @param sourceChain - Chain to move inventory from
|
|
1402
1446
|
* @param targetChain - Chain to move inventory to (origin chain for rebalancing)
|
|
1403
1447
|
* @param targetOutputAmount - Destination-local amount to receive
|
|
1404
1448
|
* @param maxSourceInput - Maximum source-local amount available for this plan
|
|
1449
|
+
* @param quoteMode - Whether to execute this bridge plan as exact-input or exact-output
|
|
1405
1450
|
* @param intent - Rebalance intent for tracking
|
|
1406
1451
|
* @param externalBridgeType - External bridge type to use
|
|
1407
1452
|
* @returns Result with success status and optional txHash/error
|
|
@@ -1411,9 +1456,10 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1411
1456
|
targetChain: ChainName,
|
|
1412
1457
|
targetOutputAmount: bigint,
|
|
1413
1458
|
maxSourceInput: bigint,
|
|
1459
|
+
quoteMode: BridgeQuoteMode,
|
|
1414
1460
|
intent: RebalanceIntent,
|
|
1415
1461
|
externalBridgeType: ExternalBridgeType,
|
|
1416
|
-
): Promise<
|
|
1462
|
+
): Promise<InventoryMovementExecutionResult> {
|
|
1417
1463
|
const sourceToken = this.getTokenForChain(sourceChain);
|
|
1418
1464
|
if (!sourceToken) {
|
|
1419
1465
|
return {
|
|
@@ -1461,15 +1507,43 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1461
1507
|
|
|
1462
1508
|
try {
|
|
1463
1509
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1510
|
+
const fromAddress = this.getInventorySignerAddress(sourceChain);
|
|
1511
|
+
const toAddress = this.getInventorySignerAddress(targetChain);
|
|
1512
|
+
const quoteWithMode = async (mode: BridgeQuoteMode) =>
|
|
1513
|
+
externalBridge.quote({
|
|
1514
|
+
fromChain: sourceChainId,
|
|
1515
|
+
toChain: targetChainId,
|
|
1516
|
+
fromToken: fromTokenAddress,
|
|
1517
|
+
toToken: toTokenAddress,
|
|
1518
|
+
...(mode === 'forward'
|
|
1519
|
+
? { fromAmount: maxSourceInput }
|
|
1520
|
+
: { toAmount: targetOutputAmount }),
|
|
1521
|
+
fromAddress,
|
|
1522
|
+
toAddress,
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
let quoteModeUsed = quoteMode;
|
|
1526
|
+
let quote = await quoteWithMode(quoteModeUsed);
|
|
1527
|
+
|
|
1528
|
+
if (quoteModeUsed === 'reverse' && quote.fromAmount > maxSourceInput) {
|
|
1529
|
+
this.logger.warn(
|
|
1530
|
+
{
|
|
1531
|
+
sourceChain,
|
|
1532
|
+
targetChain,
|
|
1533
|
+
plannedQuoteMode: quoteMode,
|
|
1534
|
+
requestedTargetOutput: targetOutputAmount.toString(),
|
|
1535
|
+
quotedInput: quote.fromAmount.toString(),
|
|
1536
|
+
maxSourceInput: maxSourceInput.toString(),
|
|
1537
|
+
intentId: intent.id,
|
|
1538
|
+
},
|
|
1539
|
+
'Reverse bridge quote exceeded source capacity, retrying with forward quote',
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
// Spend the full source cap on fallback; minor output drift is acceptable
|
|
1543
|
+
// and will be reconciled by later cycles rather than risking livelock.
|
|
1544
|
+
quoteModeUsed = 'forward';
|
|
1545
|
+
quote = await quoteWithMode(quoteModeUsed);
|
|
1546
|
+
}
|
|
1473
1547
|
|
|
1474
1548
|
const inputRequired = quote.fromAmount;
|
|
1475
1549
|
if (inputRequired > maxSourceInput) {
|
|
@@ -1490,22 +1564,30 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1490
1564
|
targetOutputAmount,
|
|
1491
1565
|
targetToken,
|
|
1492
1566
|
),
|
|
1567
|
+
quoteModePlanned: quoteMode,
|
|
1568
|
+
quoteModeUsed,
|
|
1569
|
+
retriedAsForward:
|
|
1570
|
+
quoteMode === 'reverse' && quoteModeUsed === 'forward',
|
|
1493
1571
|
inputRequired: inputRequired.toString(),
|
|
1494
1572
|
inputRequiredFormatted: this.formatLocalAmount(
|
|
1495
1573
|
inputRequired,
|
|
1496
1574
|
sourceToken,
|
|
1497
1575
|
),
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1576
|
+
quotedOutput: quote.toAmount.toString(),
|
|
1577
|
+
quotedOutputMin: quote.toAmountMin.toString(),
|
|
1578
|
+
quotedOutputFormatted: this.formatLocalAmount(
|
|
1501
1579
|
quote.toAmount,
|
|
1502
1580
|
targetToken,
|
|
1503
1581
|
),
|
|
1582
|
+
quotedOutputMinFormatted: this.formatLocalAmount(
|
|
1583
|
+
quote.toAmountMin,
|
|
1584
|
+
targetToken,
|
|
1585
|
+
),
|
|
1504
1586
|
gasCosts: quote.gasCosts.toString(),
|
|
1505
1587
|
feeCosts: quote.feeCosts.toString(),
|
|
1506
1588
|
intentId: intent.id,
|
|
1507
1589
|
},
|
|
1508
|
-
'Executing inventory movement via
|
|
1590
|
+
'Executing inventory movement via bridge quote',
|
|
1509
1591
|
);
|
|
1510
1592
|
|
|
1511
1593
|
this.logger.debug(
|
|
@@ -1573,22 +1655,31 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1573
1655
|
'Updated consumed inventory after LiFi bridge',
|
|
1574
1656
|
);
|
|
1575
1657
|
|
|
1576
|
-
return {
|
|
1658
|
+
return {
|
|
1659
|
+
success: true,
|
|
1660
|
+
txHash: result.txHash,
|
|
1661
|
+
inputRequired,
|
|
1662
|
+
quotedOutput: quote.toAmount,
|
|
1663
|
+
quotedOutputMin: quote.toAmountMin,
|
|
1664
|
+
quoteModeUsed,
|
|
1665
|
+
};
|
|
1577
1666
|
} catch (error) {
|
|
1667
|
+
const errorMessage =
|
|
1668
|
+
error instanceof Error ? error.message : String(error);
|
|
1578
1669
|
this.logger.error(
|
|
1579
1670
|
{
|
|
1580
1671
|
sourceChain,
|
|
1581
1672
|
targetChain,
|
|
1582
1673
|
amount: targetOutputAmount.toString(),
|
|
1583
1674
|
intentId: intent.id,
|
|
1584
|
-
error:
|
|
1675
|
+
error: errorMessage,
|
|
1585
1676
|
},
|
|
1586
1677
|
'Failed to execute inventory movement',
|
|
1587
1678
|
);
|
|
1588
1679
|
|
|
1589
1680
|
return {
|
|
1590
1681
|
success: false,
|
|
1591
|
-
error:
|
|
1682
|
+
error: errorMessage,
|
|
1592
1683
|
};
|
|
1593
1684
|
}
|
|
1594
1685
|
}
|
|
@@ -218,6 +218,36 @@ describe('RebalancerContextFactory', () => {
|
|
|
218
218
|
expect(multiProvider.getProvider.firstCall.args[0]).to.equal('ethereum');
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
it('should initialize providers for Tron chains (EVM-like)', async () => {
|
|
222
|
+
const { multiProvider } = createMockMultiProvider([
|
|
223
|
+
{ name: 'ethereum', protocol: ProtocolType.Ethereum },
|
|
224
|
+
{ name: 'tron', protocol: ProtocolType.Tron },
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
await callCreate(multiProvider, {
|
|
228
|
+
tokens: [
|
|
229
|
+
createToken(
|
|
230
|
+
'ethereum',
|
|
231
|
+
TEST_ADDRESSES.ethereum,
|
|
232
|
+
TokenStandard.EvmHypCollateral,
|
|
233
|
+
),
|
|
234
|
+
createToken(
|
|
235
|
+
'tron',
|
|
236
|
+
'0xTronToken1234567890',
|
|
237
|
+
TokenStandard.TronHypCollateral,
|
|
238
|
+
),
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Tron is EVM-like, so getProvider should be called for both chains
|
|
243
|
+
expect(multiProvider.getProvider.callCount).to.equal(2);
|
|
244
|
+
const providerChains = multiProvider.getProvider
|
|
245
|
+
.getCalls()
|
|
246
|
+
.map((c) => c.args[0]);
|
|
247
|
+
expect(providerChains).to.include('ethereum');
|
|
248
|
+
expect(providerChains).to.include('tron');
|
|
249
|
+
});
|
|
250
|
+
|
|
221
251
|
it('should call getProvider for all chains when all are EVM', async () => {
|
|
222
252
|
const { multiProvider } = createMockMultiProvider([
|
|
223
253
|
{ name: 'ethereum', protocol: ProtocolType.Ethereum },
|
|
@@ -363,7 +393,7 @@ describe('RebalancerContextFactory', () => {
|
|
|
363
393
|
createToken(
|
|
364
394
|
evmChain,
|
|
365
395
|
TEST_ADDRESSES.ethereum,
|
|
366
|
-
TokenStandard.
|
|
396
|
+
TokenStandard.EvmHypCollateral,
|
|
367
397
|
),
|
|
368
398
|
createToken(
|
|
369
399
|
sealevelChain,
|
|
@@ -446,6 +476,91 @@ describe('RebalancerContextFactory', () => {
|
|
|
446
476
|
);
|
|
447
477
|
});
|
|
448
478
|
|
|
479
|
+
it('should accept Tron as a supported inventory protocol', async () => {
|
|
480
|
+
const tronChain = 'tron';
|
|
481
|
+
const evmChain = 'ethereum';
|
|
482
|
+
const { multiProvider } = createMockMultiProvider([
|
|
483
|
+
{ name: evmChain, protocol: ProtocolType.Ethereum },
|
|
484
|
+
{ name: tronChain, protocol: ProtocolType.Tron },
|
|
485
|
+
]);
|
|
486
|
+
|
|
487
|
+
const config = {
|
|
488
|
+
warpRouteId: 'USDC/tron-route',
|
|
489
|
+
strategyConfig: [
|
|
490
|
+
{
|
|
491
|
+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
|
|
492
|
+
chains: {
|
|
493
|
+
[tronChain]: {
|
|
494
|
+
bridge: TEST_ADDRESSES.bridge,
|
|
495
|
+
weighted: { weight: 50n, tolerance: 10n },
|
|
496
|
+
override: {
|
|
497
|
+
[evmChain]: {
|
|
498
|
+
executionType: ExecutionType.Inventory,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
[evmChain]: {
|
|
503
|
+
bridge: TEST_ADDRESSES.bridge,
|
|
504
|
+
weighted: { weight: 50n, tolerance: 10n },
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
inventorySigners: {
|
|
510
|
+
[ProtocolType.Ethereum]: {
|
|
511
|
+
address: TEST_ADDRESSES.ethereum,
|
|
512
|
+
key: '0xabc123',
|
|
513
|
+
},
|
|
514
|
+
[ProtocolType.Tron]: {
|
|
515
|
+
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
516
|
+
key: '0xdef456',
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
intentTTL: DEFAULT_INTENT_TTL_MS,
|
|
520
|
+
} as RebalancerConfig;
|
|
521
|
+
|
|
522
|
+
const factory = await createFactory(config, multiProvider, {
|
|
523
|
+
tokens: [
|
|
524
|
+
createToken(
|
|
525
|
+
evmChain,
|
|
526
|
+
TEST_ADDRESSES.ethereum,
|
|
527
|
+
TokenStandard.EvmHypCollateral,
|
|
528
|
+
),
|
|
529
|
+
createToken(
|
|
530
|
+
tronChain,
|
|
531
|
+
'0xTronToken123',
|
|
532
|
+
TokenStandard.TronHypCollateral,
|
|
533
|
+
),
|
|
534
|
+
],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const getChainMetadataStub = factory.getWarpCore().multiProvider
|
|
538
|
+
.getChainMetadata as Sinon.SinonStub;
|
|
539
|
+
getChainMetadataStub.callsFake((chainName: string) => ({
|
|
540
|
+
protocol:
|
|
541
|
+
chainName === tronChain ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
const result = await (factory as any).createInventoryRebalancerAndConfig(
|
|
545
|
+
{} as any,
|
|
546
|
+
{},
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
assert(
|
|
550
|
+
result,
|
|
551
|
+
'Expected inventory config to be created for Tron support',
|
|
552
|
+
);
|
|
553
|
+
expect(result.inventoryConfig.inventoryAddresses).to.deep.equal({
|
|
554
|
+
[ProtocolType.Ethereum]: TEST_ADDRESSES.ethereum,
|
|
555
|
+
[ProtocolType.Tron]: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
556
|
+
});
|
|
557
|
+
expect(result.inventoryConfig.chains).to.include.members([
|
|
558
|
+
evmChain,
|
|
559
|
+
tronChain,
|
|
560
|
+
]);
|
|
561
|
+
expect(result.inventoryRebalancer).to.exist;
|
|
562
|
+
});
|
|
563
|
+
|
|
449
564
|
it('should fail early when inventory chain uses unsupported protocol', async () => {
|
|
450
565
|
const cosmosChain = 'cosmoshub';
|
|
451
566
|
const evmChain = 'ethereum';
|
|
@@ -11,7 +11,13 @@ import {
|
|
|
11
11
|
WarpCore,
|
|
12
12
|
type WarpCoreConfig,
|
|
13
13
|
} from '@hyperlane-xyz/sdk';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Address,
|
|
16
|
+
assert,
|
|
17
|
+
isEVMLike,
|
|
18
|
+
ProtocolType,
|
|
19
|
+
objMap,
|
|
20
|
+
} from '@hyperlane-xyz/utils';
|
|
15
21
|
|
|
16
22
|
import { LiFiBridge } from '../bridges/LiFiBridge.js';
|
|
17
23
|
import { type RebalancerConfig } from '../config/RebalancerConfig.js';
|
|
@@ -137,7 +143,7 @@ export class RebalancerContextFactory {
|
|
|
137
143
|
...new Set(warpCoreConfig.tokens.map((t) => t.chainName)),
|
|
138
144
|
];
|
|
139
145
|
for (const chain of warpChains) {
|
|
140
|
-
if (multiProvider.getProtocol(chain)
|
|
146
|
+
if (!isEVMLike(multiProvider.getProtocol(chain))) {
|
|
141
147
|
logger.debug({ chain }, 'Skipping provider init for non-EVM chain');
|
|
142
148
|
continue;
|
|
143
149
|
}
|
|
@@ -373,11 +379,12 @@ export class RebalancerContextFactory {
|
|
|
373
379
|
bridges,
|
|
374
380
|
rebalancerAddress,
|
|
375
381
|
inventorySignerAddresses: this.config.inventorySigners
|
|
376
|
-
?
|
|
377
|
-
.filter((
|
|
378
|
-
.map(
|
|
379
|
-
|
|
380
|
-
|
|
382
|
+
? Object.values(ProtocolType)
|
|
383
|
+
.filter((protocol) => isEVMLike(protocol))
|
|
384
|
+
.map(
|
|
385
|
+
(protocol) => this.config.inventorySigners?.[protocol]?.address,
|
|
386
|
+
)
|
|
387
|
+
.filter((address): address is Address => Boolean(address))
|
|
381
388
|
: undefined,
|
|
382
389
|
intentTTL: this.config.intentTTL,
|
|
383
390
|
};
|
|
@@ -481,6 +488,7 @@ export class RebalancerContextFactory {
|
|
|
481
488
|
const SUPPORTED_INVENTORY_PROTOCOLS = new Set([
|
|
482
489
|
ProtocolType.Ethereum,
|
|
483
490
|
ProtocolType.Sealevel,
|
|
491
|
+
ProtocolType.Tron,
|
|
484
492
|
]);
|
|
485
493
|
for (const protocol of requiredProtocols) {
|
|
486
494
|
const chainsForProtocol = allRelevantChains.filter(
|
|
@@ -490,7 +498,7 @@ export class RebalancerContextFactory {
|
|
|
490
498
|
);
|
|
491
499
|
assert(
|
|
492
500
|
SUPPORTED_INVENTORY_PROTOCOLS.has(protocol),
|
|
493
|
-
`Inventory rebalancing does not support protocol '${protocol}' (chains: ${chainsForProtocol.join(', ')}). Supported: ethereum, sealevel`,
|
|
501
|
+
`Inventory rebalancing does not support protocol '${protocol}' (chains: ${chainsForProtocol.join(', ')}). Supported: ethereum, sealevel, tron`,
|
|
494
502
|
);
|
|
495
503
|
}
|
|
496
504
|
for (const protocol of requiredProtocols) {
|
|
@@ -532,12 +540,12 @@ export class RebalancerContextFactory {
|
|
|
532
540
|
'No external bridges configured, skipping inventory components',
|
|
533
541
|
);
|
|
534
542
|
}
|
|
535
|
-
const inventoryAddresses =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
]
|
|
540
|
-
|
|
543
|
+
const inventoryAddresses: Partial<Record<ProtocolType, Address>> = {};
|
|
544
|
+
for (const protocol of Object.values(ProtocolType)) {
|
|
545
|
+
const cfg = inventorySigners[protocol];
|
|
546
|
+
if (!cfg) continue;
|
|
547
|
+
inventoryAddresses[protocol] = cfg.address;
|
|
548
|
+
}
|
|
541
549
|
const inventoryConfig: InventoryMonitorConfig = {
|
|
542
550
|
inventoryAddresses,
|
|
543
551
|
chains: allRelevantChains,
|
|
@@ -546,11 +554,12 @@ export class RebalancerContextFactory {
|
|
|
546
554
|
const mergedSigners: Partial<
|
|
547
555
|
Record<ProtocolType, InventorySignerConfig>
|
|
548
556
|
> = {};
|
|
549
|
-
for (const
|
|
550
|
-
const
|
|
551
|
-
|
|
557
|
+
for (const protocol of Object.values(ProtocolType)) {
|
|
558
|
+
const cfg = inventorySigners[protocol];
|
|
559
|
+
if (!cfg) continue;
|
|
560
|
+
mergedSigners[protocol] = {
|
|
552
561
|
address: cfg.address,
|
|
553
|
-
key: cfg.key ?? this.inventorySignerKeysByProtocol?.[
|
|
562
|
+
key: cfg.key ?? this.inventorySignerKeysByProtocol?.[protocol],
|
|
554
563
|
};
|
|
555
564
|
}
|
|
556
565
|
const inventoryRebalancer = new InventoryRebalancer(
|
|
@@ -572,12 +581,12 @@ export class RebalancerContextFactory {
|
|
|
572
581
|
}
|
|
573
582
|
|
|
574
583
|
// 3. Build inventory config
|
|
575
|
-
const inventoryAddresses =
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
]
|
|
580
|
-
|
|
584
|
+
const inventoryAddresses: Partial<Record<ProtocolType, Address>> = {};
|
|
585
|
+
for (const protocol of Object.values(ProtocolType)) {
|
|
586
|
+
const cfg = inventorySigners[protocol];
|
|
587
|
+
if (!cfg) continue;
|
|
588
|
+
inventoryAddresses[protocol] = cfg.address;
|
|
589
|
+
}
|
|
581
590
|
const inventoryConfig: InventoryMonitorConfig = {
|
|
582
591
|
inventoryAddresses,
|
|
583
592
|
chains: allRelevantChains,
|
|
@@ -587,11 +596,12 @@ export class RebalancerContextFactory {
|
|
|
587
596
|
// Merge config addresses with runtime keys
|
|
588
597
|
const mergedSigners: Partial<Record<ProtocolType, InventorySignerConfig>> =
|
|
589
598
|
{};
|
|
590
|
-
for (const
|
|
591
|
-
const
|
|
592
|
-
|
|
599
|
+
for (const protocol of Object.values(ProtocolType)) {
|
|
600
|
+
const cfg = inventorySigners[protocol];
|
|
601
|
+
if (!cfg) continue;
|
|
602
|
+
mergedSigners[protocol] = {
|
|
593
603
|
address: cfg.address,
|
|
594
|
-
key: cfg.key ?? this.inventorySignerKeysByProtocol?.[
|
|
604
|
+
key: cfg.key ?? this.inventorySignerKeysByProtocol?.[protocol],
|
|
595
605
|
};
|
|
596
606
|
}
|
|
597
607
|
const inventoryRebalancer = new InventoryRebalancer(
|
package/src/service.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { MultiProvider } from '@hyperlane-xyz/sdk';
|
|
|
33
33
|
import {
|
|
34
34
|
applyRpcUrlOverridesFromEnv,
|
|
35
35
|
createServiceLogger,
|
|
36
|
+
isEVMLike,
|
|
36
37
|
ProtocolType,
|
|
37
38
|
rootLogger,
|
|
38
39
|
} from '@hyperlane-xyz/utils';
|
|
@@ -151,12 +152,15 @@ async function main(): Promise<void> {
|
|
|
151
152
|
Record<ProtocolType, InventorySignerConfig>
|
|
152
153
|
> = {};
|
|
153
154
|
|
|
154
|
-
for (const
|
|
155
|
+
for (const protocol of Object.values(ProtocolType)) {
|
|
156
|
+
const privateKey = inventoryPrivateKeys[protocol];
|
|
155
157
|
if (!privateKey) continue;
|
|
156
158
|
|
|
157
159
|
let derivedAddress: string;
|
|
158
160
|
|
|
159
|
-
if (protocol
|
|
161
|
+
if (isEVMLike(protocol)) {
|
|
162
|
+
// Tron uses same hex private key format as Ethereum.
|
|
163
|
+
// Derive 0x-prefixed hex address via ethers Wallet (TronWallet extends Wallet).
|
|
160
164
|
derivedAddress = new Wallet(privateKey).address;
|
|
161
165
|
} else if (protocol === ProtocolType.Sealevel) {
|
|
162
166
|
const keyBytes = parseSolanaPrivateKey(privateKey);
|
|
@@ -172,12 +176,11 @@ async function main(): Promise<void> {
|
|
|
172
176
|
|
|
173
177
|
// Validate against config if present
|
|
174
178
|
const configuredAddress =
|
|
175
|
-
rebalancerConfig.inventorySigners?.[protocol
|
|
179
|
+
rebalancerConfig.inventorySigners?.[protocol]?.address;
|
|
176
180
|
if (configuredAddress) {
|
|
177
|
-
const mismatch =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
: configuredAddress !== derivedAddress;
|
|
181
|
+
const mismatch = isEVMLike(protocol)
|
|
182
|
+
? configuredAddress.toLowerCase() !== derivedAddress.toLowerCase()
|
|
183
|
+
: configuredAddress !== derivedAddress;
|
|
181
184
|
if (mismatch) {
|
|
182
185
|
throw new Error(
|
|
183
186
|
`inventorySigners.${protocol} mismatch: config has ${configuredAddress} but HYP_INVENTORY_KEY_${protocol.toUpperCase()} derives to ${derivedAddress}`,
|
|
@@ -185,7 +188,7 @@ async function main(): Promise<void> {
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
inventorySigners[protocol
|
|
191
|
+
inventorySigners[protocol] = {
|
|
189
192
|
address: derivedAddress,
|
|
190
193
|
key: privateKey,
|
|
191
194
|
};
|