@hyperlane-xyz/rebalancer 27.2.14 → 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 +59 -51
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +176 -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.map +1 -1
- package/dist/core/InventoryRebalancer.js +3 -2
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +108 -1
- 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 +227 -0
- package/src/bridges/LiFiBridge.ts +82 -67
- package/src/config/RebalancerConfig.test.ts +135 -0
- package/src/config/types.ts +8 -0
- package/src/core/InventoryRebalancer.test.ts +160 -0
- package/src/core/InventoryRebalancer.ts +9 -3
- 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
|
@@ -572,6 +572,141 @@ describe('RebalancerConfig', () => {
|
|
|
572
572
|
});
|
|
573
573
|
});
|
|
574
574
|
|
|
575
|
+
describe('Tron inventorySigners validation', () => {
|
|
576
|
+
const TEST_CONFIG_PATH_TRON = join(tmpdir(), 'rebalancer-tron-test.yaml');
|
|
577
|
+
|
|
578
|
+
afterEach(() => {
|
|
579
|
+
rmSync(TEST_CONFIG_PATH_TRON, { force: true });
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('should reject Tron T-prefix base58 address in inventorySigners', () => {
|
|
583
|
+
const data: RebalancerConfigFileInput = {
|
|
584
|
+
warpRouteId: 'test-tron-route',
|
|
585
|
+
strategy: [
|
|
586
|
+
{
|
|
587
|
+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
|
|
588
|
+
chains: {
|
|
589
|
+
tron: {
|
|
590
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
591
|
+
executionType: ExecutionType.Inventory,
|
|
592
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
593
|
+
},
|
|
594
|
+
ethereum: {
|
|
595
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
596
|
+
executionType: ExecutionType.Inventory,
|
|
597
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
inventorySigners: {
|
|
603
|
+
tron: {
|
|
604
|
+
address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
|
|
605
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
606
|
+
},
|
|
607
|
+
ethereum: {
|
|
608
|
+
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
609
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
externalBridges: {
|
|
613
|
+
lifi: {
|
|
614
|
+
integrator: 'test-app',
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
writeYamlOrJson(TEST_CONFIG_PATH_TRON, data);
|
|
620
|
+
expect(() => RebalancerConfig.load(TEST_CONFIG_PATH_TRON)).to.throw(
|
|
621
|
+
/must be a valid 0x hex address/,
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should accept valid 0x hex address for Tron inventorySigners', () => {
|
|
626
|
+
const data: RebalancerConfigFileInput = {
|
|
627
|
+
warpRouteId: 'test-tron-route',
|
|
628
|
+
strategy: [
|
|
629
|
+
{
|
|
630
|
+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
|
|
631
|
+
chains: {
|
|
632
|
+
tron: {
|
|
633
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
634
|
+
executionType: ExecutionType.Inventory,
|
|
635
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
636
|
+
},
|
|
637
|
+
ethereum: {
|
|
638
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
639
|
+
executionType: ExecutionType.Inventory,
|
|
640
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
inventorySigners: {
|
|
646
|
+
tron: {
|
|
647
|
+
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
648
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
649
|
+
},
|
|
650
|
+
ethereum: {
|
|
651
|
+
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
652
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
externalBridges: {
|
|
656
|
+
lifi: {
|
|
657
|
+
integrator: 'test-app',
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
writeYamlOrJson(TEST_CONFIG_PATH_TRON, data);
|
|
663
|
+
expect(() => RebalancerConfig.load(TEST_CONFIG_PATH_TRON)).to.not.throw();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should reject invalid Tron address in inventorySigners', () => {
|
|
667
|
+
const data: RebalancerConfigFileInput = {
|
|
668
|
+
warpRouteId: 'test-tron-route',
|
|
669
|
+
strategy: [
|
|
670
|
+
{
|
|
671
|
+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
|
|
672
|
+
chains: {
|
|
673
|
+
tron: {
|
|
674
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
675
|
+
executionType: ExecutionType.Inventory,
|
|
676
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
677
|
+
},
|
|
678
|
+
ethereum: {
|
|
679
|
+
weighted: { weight: 50, tolerance: 5 },
|
|
680
|
+
executionType: ExecutionType.Inventory,
|
|
681
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
inventorySigners: {
|
|
687
|
+
tron: {
|
|
688
|
+
address: 'INVALID_TRON_ADDRESS',
|
|
689
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
690
|
+
},
|
|
691
|
+
ethereum: {
|
|
692
|
+
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
|
693
|
+
key: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
externalBridges: {
|
|
697
|
+
lifi: {
|
|
698
|
+
integrator: 'test-app',
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
writeYamlOrJson(TEST_CONFIG_PATH_TRON, data);
|
|
704
|
+
expect(() => RebalancerConfig.load(TEST_CONFIG_PATH_TRON)).to.throw(
|
|
705
|
+
/must be a valid 0x hex address/,
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
575
710
|
describe('per-chain bridge configuration', () => {
|
|
576
711
|
const TEST_CONFIG_PATH_BRIDGE = join(tmpdir(), 'rebalancer-bridge-test.yaml');
|
|
577
712
|
|
package/src/config/types.ts
CHANGED
|
@@ -361,6 +361,14 @@ export const RebalancerConfigSchema = z
|
|
|
361
361
|
path: ['inventorySigners', protocol],
|
|
362
362
|
});
|
|
363
363
|
}
|
|
364
|
+
} else if (protocol === ProtocolType.Tron) {
|
|
365
|
+
if (!isAddressEvm(signerConfig.address)) {
|
|
366
|
+
ctx.addIssue({
|
|
367
|
+
code: z.ZodIssueCode.custom,
|
|
368
|
+
message: `inventorySigners.${protocol} must be a valid 0x hex address, got: ${signerConfig.address}`,
|
|
369
|
+
path: ['inventorySigners', protocol],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
364
372
|
}
|
|
365
373
|
// Other protocols: accept any non-empty string (future-proof)
|
|
366
374
|
}
|
|
@@ -7,6 +7,7 @@ import Sinon, { type SinonStubbedInstance } from 'sinon';
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
type ChainName,
|
|
10
|
+
HyperlaneCore,
|
|
10
11
|
type MultiProvider,
|
|
11
12
|
ProviderType,
|
|
12
13
|
TokenStandard,
|
|
@@ -444,6 +445,165 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
444
445
|
});
|
|
445
446
|
});
|
|
446
447
|
|
|
448
|
+
it('sendAndConfirmInventoryTx uses the EVM-like signer path for Tron txs', async () => {
|
|
449
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
450
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
451
|
+
address: INVENTORY_SIGNER,
|
|
452
|
+
key: TEST_PRIVATE_KEY,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const getChainMetadata = (chain: ChainName) => ({
|
|
456
|
+
name: chain,
|
|
457
|
+
protocol:
|
|
458
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
459
|
+
blocks: { reorgPeriod: 2 },
|
|
460
|
+
rpcUrls: [{ http: 'http://127.0.0.1:9090/jsonrpc' }],
|
|
461
|
+
});
|
|
462
|
+
warpCore.multiProvider.getChainMetadata.callsFake(getChainMetadata);
|
|
463
|
+
multiProvider.getChainMetadata.callsFake(getChainMetadata);
|
|
464
|
+
warpCore.multiProvider.toMultiProvider =
|
|
465
|
+
Sinon.stub().returns(multiProvider);
|
|
466
|
+
|
|
467
|
+
const sendFn = (inventoryRebalancer as any).sendAndConfirmInventoryTx.bind(
|
|
468
|
+
inventoryRebalancer,
|
|
469
|
+
);
|
|
470
|
+
const result = await sendFn(TRON_CHAIN, {
|
|
471
|
+
category: WarpTxCategory.Transfer,
|
|
472
|
+
type: ProviderType.Tron,
|
|
473
|
+
transaction: {
|
|
474
|
+
to: '0xRouterAddress',
|
|
475
|
+
data: '0xTransferRemoteData',
|
|
476
|
+
value: 1000000n,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(result).to.deep.equal({ txHash: '0xTransferRemoteTxHash' });
|
|
481
|
+
expect(multiProvider.setSigner.calledOnce).to.be.true;
|
|
482
|
+
expect(
|
|
483
|
+
multiProvider.sendTransaction.calledOnceWithExactly(
|
|
484
|
+
TRON_CHAIN,
|
|
485
|
+
{
|
|
486
|
+
to: '0xRouterAddress',
|
|
487
|
+
data: '0xTransferRemoteData',
|
|
488
|
+
value: 1000000n,
|
|
489
|
+
},
|
|
490
|
+
{ waitConfirmations: 2 },
|
|
491
|
+
),
|
|
492
|
+
).to.be.true;
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('buildSignerAccountConfig returns correct config for Tron protocol', () => {
|
|
496
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
497
|
+
address: INVENTORY_SIGNER,
|
|
498
|
+
key: TEST_PRIVATE_KEY,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const buildFn = (inventoryRebalancer as any).buildSignerAccountConfig.bind(
|
|
502
|
+
inventoryRebalancer,
|
|
503
|
+
);
|
|
504
|
+
const result = buildFn(
|
|
505
|
+
ProtocolType.Tron,
|
|
506
|
+
TEST_PRIVATE_KEY,
|
|
507
|
+
'tron' as ChainName,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
expect(result.protocol).to.equal(ProtocolType.Tron);
|
|
511
|
+
expect(result.privateKey).to.equal(TEST_PRIVATE_KEY);
|
|
512
|
+
// Tron uses same 0x-prefixed hex key format as Ethereum
|
|
513
|
+
expect(result.privateKey.startsWith('0x')).to.be.true;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('getTransactionReceipt returns EthersV5 type for Tron chain', async () => {
|
|
517
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
518
|
+
const mockReceipt = {
|
|
519
|
+
transactionHash: '0xTronReceipt',
|
|
520
|
+
logs: [],
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
524
|
+
name: chain,
|
|
525
|
+
protocol:
|
|
526
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
warpCore.multiProvider.getEthersV5Provider.returns({
|
|
530
|
+
getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const getReceiptFn = (
|
|
534
|
+
inventoryRebalancer as any
|
|
535
|
+
).getTransactionReceipt.bind(inventoryRebalancer);
|
|
536
|
+
const receipt = await getReceiptFn(TRON_CHAIN, '0xTronTxHash');
|
|
537
|
+
|
|
538
|
+
expect(receipt).to.not.be.undefined;
|
|
539
|
+
expect(receipt.type).to.equal(ProviderType.EthersV5);
|
|
540
|
+
expect(receipt.receipt).to.deep.equal(mockReceipt);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('extractDispatchedMessageId returns Tron message ids via the EthersV5 path', async () => {
|
|
544
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
545
|
+
const expectedMessageId =
|
|
546
|
+
'0x1111111111111111111111111111111111111111111111111111111111111111';
|
|
547
|
+
const mockReceipt = {
|
|
548
|
+
transactionHash: '0xTronTx',
|
|
549
|
+
logs: [{}],
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
553
|
+
name: chain,
|
|
554
|
+
protocol:
|
|
555
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
const getDispatchedMessagesStub = Sinon.stub(
|
|
559
|
+
HyperlaneCore,
|
|
560
|
+
'getDispatchedMessages',
|
|
561
|
+
).returns([{ id: expectedMessageId }] as any);
|
|
562
|
+
warpCore.multiProvider.getEthersV5Provider.returns({
|
|
563
|
+
getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const extractFn = (
|
|
567
|
+
inventoryRebalancer as any
|
|
568
|
+
).extractDispatchedMessageId.bind(inventoryRebalancer);
|
|
569
|
+
const messageId = await extractFn(TRON_CHAIN, '0xTronTx');
|
|
570
|
+
expect(messageId).to.equal(expectedMessageId);
|
|
571
|
+
expect(getDispatchedMessagesStub.calledOnce).to.be.true;
|
|
572
|
+
expect(getDispatchedMessagesStub.firstCall.firstArg).to.equal(mockReceipt);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('selects the Tron signer address from a mixed multi-protocol config', () => {
|
|
576
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
577
|
+
address: INVENTORY_SIGNER,
|
|
578
|
+
key: TEST_PRIVATE_KEY,
|
|
579
|
+
};
|
|
580
|
+
config.inventorySigners[ProtocolType.Sealevel] = {
|
|
581
|
+
address: 'SoLANAAddReSs1234567890123456789012345678',
|
|
582
|
+
key: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32',
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
586
|
+
name: chain,
|
|
587
|
+
protocol:
|
|
588
|
+
chain === 'tron'
|
|
589
|
+
? ProtocolType.Tron
|
|
590
|
+
: chain === SOLANA_CHAIN
|
|
591
|
+
? ProtocolType.Sealevel
|
|
592
|
+
: ProtocolType.Ethereum,
|
|
593
|
+
}));
|
|
594
|
+
|
|
595
|
+
const getInventorySignerAddress = (
|
|
596
|
+
inventoryRebalancer as any
|
|
597
|
+
).getInventorySignerAddress.bind(inventoryRebalancer);
|
|
598
|
+
|
|
599
|
+
expect(getInventorySignerAddress('tron' as ChainName)).to.equal(
|
|
600
|
+
INVENTORY_SIGNER,
|
|
601
|
+
);
|
|
602
|
+
expect(getInventorySignerAddress(SOLANA_CHAIN)).to.equal(
|
|
603
|
+
'SoLANAAddReSs1234567890123456789012345678',
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
|
|
447
607
|
describe('Partial Fulfillment (Insufficient Inventory)', () => {
|
|
448
608
|
// Partial transfers happen when maxTransferable >= minViableTransfer.
|
|
449
609
|
// For non-native tokens (EvmHypCollateral), minViableTransfer = 0, so any
|
|
@@ -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 {
|
|
@@ -89,7 +95,6 @@ type InventoryMovementExecutionResult =
|
|
|
89
95
|
success: false;
|
|
90
96
|
error: string;
|
|
91
97
|
};
|
|
92
|
-
|
|
93
98
|
const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
|
|
94
99
|
'balance may be insufficient',
|
|
95
100
|
'transfer amount exceeds balance',
|
|
@@ -1221,6 +1226,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1221
1226
|
): MultiProtocolSignerSignerAccountInfo {
|
|
1222
1227
|
void chain;
|
|
1223
1228
|
switch (protocol) {
|
|
1229
|
+
case ProtocolType.Tron:
|
|
1224
1230
|
case ProtocolType.Ethereum:
|
|
1225
1231
|
return { protocol, privateKey: ensure0x(key) };
|
|
1226
1232
|
case ProtocolType.Sealevel:
|
|
@@ -1260,7 +1266,7 @@ export class InventoryRebalancer implements IInventoryRebalancer {
|
|
|
1260
1266
|
try {
|
|
1261
1267
|
const protocol = this.getProtocolForChain(origin);
|
|
1262
1268
|
|
|
1263
|
-
if (protocol
|
|
1269
|
+
if (isEVMLike(protocol)) {
|
|
1264
1270
|
const provider =
|
|
1265
1271
|
this.warpCore.multiProvider.getEthersV5Provider(origin);
|
|
1266
1272
|
const receipt = await provider.getTransactionReceipt(txHash);
|
|
@@ -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
|
};
|