@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,6 +15,7 @@ const testLogger = pino({ level: 'silent' });
|
|
|
15
15
|
|
|
16
16
|
const TEST_PRIVATE_KEY =
|
|
17
17
|
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
18
|
+
const OTHER_PRIVATE_KEY = `${TEST_PRIVATE_KEY.slice(0, -1)}1`;
|
|
18
19
|
|
|
19
20
|
const BRIDGE_CONFIG: ExternalBridgeConfig = {
|
|
20
21
|
integrator: 'test-rebalancer',
|
|
@@ -34,6 +35,58 @@ const SOLANA_CHAIN_METADATA_CONFIG: ExternalBridgeConfig = {
|
|
|
34
35
|
},
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
const DUPLICATE_CHAIN_ID_CONFIG: ExternalBridgeConfig = {
|
|
39
|
+
integrator: 'test-rebalancer',
|
|
40
|
+
chainMetadata: {
|
|
41
|
+
ethereum: {
|
|
42
|
+
chainId: 1,
|
|
43
|
+
protocol: ProtocolType.Ethereum,
|
|
44
|
+
name: 'ethereum',
|
|
45
|
+
displayName: 'Ethereum',
|
|
46
|
+
domainId: 1,
|
|
47
|
+
rpcUrls: [{ http: 'https://ethereum-rpc.local' }],
|
|
48
|
+
},
|
|
49
|
+
radix: {
|
|
50
|
+
chainId: 1,
|
|
51
|
+
protocol: ProtocolType.Radix,
|
|
52
|
+
name: 'radix',
|
|
53
|
+
displayName: 'Radix',
|
|
54
|
+
domainId: 1001,
|
|
55
|
+
rpcUrls: [{ http: 'https://radix-rpc.local' }],
|
|
56
|
+
},
|
|
57
|
+
solanamainnet: {
|
|
58
|
+
chainId: 1399811149,
|
|
59
|
+
protocol: ProtocolType.Sealevel,
|
|
60
|
+
name: 'solanamainnet',
|
|
61
|
+
displayName: 'Solana',
|
|
62
|
+
domainId: 1399811149,
|
|
63
|
+
rpcUrls: [{ http: 'https://api.mainnet-beta.solana.com' }],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const NON_EVM_DOMAIN_COLLISION_CONFIG: ExternalBridgeConfig = {
|
|
69
|
+
integrator: 'test-rebalancer',
|
|
70
|
+
chainMetadata: {
|
|
71
|
+
ethereum: {
|
|
72
|
+
chainId: 1,
|
|
73
|
+
protocol: ProtocolType.Ethereum,
|
|
74
|
+
name: 'ethereum',
|
|
75
|
+
displayName: 'Ethereum',
|
|
76
|
+
domainId: 1,
|
|
77
|
+
rpcUrls: [{ http: 'https://ethereum-rpc.local' }],
|
|
78
|
+
},
|
|
79
|
+
cosmos: {
|
|
80
|
+
chainId: 999999999,
|
|
81
|
+
protocol: ProtocolType.Cosmos,
|
|
82
|
+
name: 'cosmos',
|
|
83
|
+
displayName: 'Cosmos',
|
|
84
|
+
domainId: 1,
|
|
85
|
+
rpcUrls: [{ http: 'https://rpc.cosmos.invalid' }],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
37
90
|
// Use all-digit hex addresses to avoid EIP-55 checksum case mutations
|
|
38
91
|
const TOKEN_ADDR = '0x1234567890123456789012345678901234567890';
|
|
39
92
|
const SENDER_ADDR = '0x9876543210987654321098765432109876543210';
|
|
@@ -557,6 +610,49 @@ describe('LiFiBridge.quote() input validation', function () {
|
|
|
557
610
|
});
|
|
558
611
|
});
|
|
559
612
|
|
|
613
|
+
describe('LiFiBridge.quote() routing policy', function () {
|
|
614
|
+
let bridge: LiFiBridge;
|
|
615
|
+
|
|
616
|
+
beforeEach(() => {
|
|
617
|
+
bridge = new LiFiBridge(BRIDGE_CONFIG, testLogger);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should use RECOMMENDED order for reverse toAmount quotes', async () => {
|
|
621
|
+
const originalFetch = globalThis.fetch;
|
|
622
|
+
let requestUrl = '';
|
|
623
|
+
|
|
624
|
+
globalThis.fetch = (async (input: URL | RequestInfo) => {
|
|
625
|
+
requestUrl = String(input);
|
|
626
|
+
return {
|
|
627
|
+
ok: true,
|
|
628
|
+
json: async () =>
|
|
629
|
+
createLiFiStep({
|
|
630
|
+
toChainId: 1151111081099710,
|
|
631
|
+
toAmount: '5000000000',
|
|
632
|
+
}),
|
|
633
|
+
} as Response;
|
|
634
|
+
}) as typeof fetch;
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const quote = await bridge.quote({
|
|
638
|
+
fromChain: 42161,
|
|
639
|
+
toChain: 1151111081099710,
|
|
640
|
+
fromToken: TOKEN_ADDR,
|
|
641
|
+
toToken: TOKEN_ADDR,
|
|
642
|
+
fromAddress: SENDER_ADDR,
|
|
643
|
+
toAddress: SENDER_ADDR,
|
|
644
|
+
toAmount: 5000000000n,
|
|
645
|
+
});
|
|
646
|
+
const params = new URL(requestUrl).searchParams;
|
|
647
|
+
|
|
648
|
+
expect(params.get('order')).to.equal('RECOMMENDED');
|
|
649
|
+
expect(quote.requestParams.toAmount).to.equal(5000000000n);
|
|
650
|
+
} finally {
|
|
651
|
+
globalThis.fetch = originalFetch;
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
560
656
|
describe('LiFiBridge.getStatus()', function () {
|
|
561
657
|
let bridge: LiFiBridge;
|
|
562
658
|
|
|
@@ -612,4 +708,178 @@ describe('LiFiBridge constructor chainMetadataByChainId', function () {
|
|
|
612
708
|
expect(msg).to.include('does not match requested');
|
|
613
709
|
}
|
|
614
710
|
});
|
|
711
|
+
|
|
712
|
+
it('should prefer Ethereum metadata for LiFi chainId lookups when non-EVM chainIds collide', () => {
|
|
713
|
+
const bridge = new LiFiBridge(DUPLICATE_CHAIN_ID_CONFIG, testLogger);
|
|
714
|
+
const quote = createTestQuote(
|
|
715
|
+
{ fromChainId: 1 },
|
|
716
|
+
{
|
|
717
|
+
fromChain: 1,
|
|
718
|
+
toChain: 1151111081099710,
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
return bridge
|
|
723
|
+
.execute(quote, {
|
|
724
|
+
[ProtocolType.Ethereum]: '0x1234',
|
|
725
|
+
})
|
|
726
|
+
.catch((error: unknown) => {
|
|
727
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
728
|
+
expect(
|
|
729
|
+
isValidationError(msg),
|
|
730
|
+
`Expected non-validation error but got: ${msg}`,
|
|
731
|
+
).to.equal(false);
|
|
732
|
+
expect(msg).to.not.include('Missing private key');
|
|
733
|
+
expect(msg).to.not.include('protocol radix');
|
|
734
|
+
expect(msg.toLowerCase()).to.include('private key');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('should index Tron by chainId even when domainId differs', () => {
|
|
739
|
+
const TRON_CHAIN_ID = 728126428;
|
|
740
|
+
const bridge = new LiFiBridge(
|
|
741
|
+
{
|
|
742
|
+
integrator: 'test-rebalancer',
|
|
743
|
+
chainMetadata: {
|
|
744
|
+
tron: {
|
|
745
|
+
chainId: TRON_CHAIN_ID,
|
|
746
|
+
protocol: ProtocolType.Tron,
|
|
747
|
+
name: 'tron',
|
|
748
|
+
displayName: 'Tron',
|
|
749
|
+
domainId: 999000999,
|
|
750
|
+
rpcUrls: [{ http: 'https://api.trongrid.io/jsonrpc' }],
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
testLogger,
|
|
755
|
+
);
|
|
756
|
+
const getProtocolTypeForChainId = (
|
|
757
|
+
bridge as any
|
|
758
|
+
).getProtocolTypeForChainId.bind(bridge);
|
|
759
|
+
|
|
760
|
+
expect(getProtocolTypeForChainId(TRON_CHAIN_ID)).to.equal(
|
|
761
|
+
ProtocolType.Tron,
|
|
762
|
+
);
|
|
763
|
+
expect(getProtocolTypeForChainId(999000999)).to.equal(undefined);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('should not let non-EVM domainIds overwrite EVM chainId lookups', () => {
|
|
767
|
+
const bridge = new LiFiBridge(NON_EVM_DOMAIN_COLLISION_CONFIG, testLogger);
|
|
768
|
+
const getProtocolTypeForChainId = (
|
|
769
|
+
bridge as any
|
|
770
|
+
).getProtocolTypeForChainId.bind(bridge);
|
|
771
|
+
|
|
772
|
+
expect(getProtocolTypeForChainId(1)).to.equal(ProtocolType.Ethereum);
|
|
773
|
+
expect(getProtocolTypeForChainId(999999999)).to.equal(ProtocolType.Cosmos);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
describe('LiFiBridge source protocol handling', function () {
|
|
778
|
+
it('ignores unrelated Tron keys when executing an Ethereum LiFi route', async () => {
|
|
779
|
+
const bridge = new LiFiBridge(BRIDGE_CONFIG, testLogger);
|
|
780
|
+
(bridge as any).configureLiFiProvider = (
|
|
781
|
+
protocol: ProtocolType,
|
|
782
|
+
key: string,
|
|
783
|
+
fromChain: number,
|
|
784
|
+
) => {
|
|
785
|
+
expect(protocol).to.equal(ProtocolType.Ethereum);
|
|
786
|
+
expect(key).to.equal(TEST_PRIVATE_KEY);
|
|
787
|
+
expect(fromChain).to.equal(42161);
|
|
788
|
+
throw new Error('expected downstream error');
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const quote = createTestQuote();
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
await bridge.execute(quote, {
|
|
795
|
+
[ProtocolType.Ethereum]: TEST_PRIVATE_KEY,
|
|
796
|
+
[ProtocolType.Tron]: OTHER_PRIVATE_KEY,
|
|
797
|
+
});
|
|
798
|
+
expect.fail('Expected execute to throw');
|
|
799
|
+
} catch (error: unknown) {
|
|
800
|
+
const msg = (error as Error).message;
|
|
801
|
+
expect(msg).to.equal('expected downstream error');
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('addressesEqual keeps Sealevel base58 comparison case-sensitive', () => {
|
|
806
|
+
const bridge = new LiFiBridge(SOLANA_CHAIN_METADATA_CONFIG, testLogger);
|
|
807
|
+
const addressesEqualFn = (bridge as any).addressesEqual.bind(bridge);
|
|
808
|
+
|
|
809
|
+
expect(
|
|
810
|
+
addressesEqualFn(
|
|
811
|
+
'SoLANAAddReSs1234567890123456789012345678',
|
|
812
|
+
'solanaaddress1234567890123456789012345678',
|
|
813
|
+
1399811149,
|
|
814
|
+
),
|
|
815
|
+
).to.be.false;
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('throws when the route source protocol is Tron', async () => {
|
|
819
|
+
const TRON_CHAIN_ID = 728126428;
|
|
820
|
+
const bridge = new LiFiBridge(
|
|
821
|
+
{
|
|
822
|
+
integrator: 'test-rebalancer',
|
|
823
|
+
chainMetadata: {
|
|
824
|
+
tron: {
|
|
825
|
+
chainId: TRON_CHAIN_ID,
|
|
826
|
+
protocol: ProtocolType.Tron,
|
|
827
|
+
name: 'tron',
|
|
828
|
+
displayName: 'Tron',
|
|
829
|
+
domainId: TRON_CHAIN_ID,
|
|
830
|
+
rpcUrls: [{ http: 'https://api.trongrid.io/jsonrpc' }],
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
testLogger,
|
|
835
|
+
);
|
|
836
|
+
const quote = createTestQuote(
|
|
837
|
+
{ fromChainId: TRON_CHAIN_ID },
|
|
838
|
+
{ fromChain: TRON_CHAIN_ID },
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
await bridge.execute(quote, {
|
|
843
|
+
[ProtocolType.Tron]: TEST_PRIVATE_KEY,
|
|
844
|
+
});
|
|
845
|
+
expect.fail('Should have thrown for unsupported source protocol Tron');
|
|
846
|
+
} catch (error: unknown) {
|
|
847
|
+
const msg = (error as Error).message;
|
|
848
|
+
expect(msg).to.include("Unsupported protocol type 'tron'");
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('throws when the route source protocol is an unsupported non-Tron chain', async () => {
|
|
853
|
+
const COSMOS_CHAIN_ID = 999999999;
|
|
854
|
+
const bridge = new LiFiBridge(
|
|
855
|
+
{
|
|
856
|
+
integrator: 'test-rebalancer',
|
|
857
|
+
chainMetadata: {
|
|
858
|
+
cosmos: {
|
|
859
|
+
chainId: COSMOS_CHAIN_ID,
|
|
860
|
+
protocol: ProtocolType.Cosmos,
|
|
861
|
+
name: 'cosmos',
|
|
862
|
+
displayName: 'Cosmos',
|
|
863
|
+
domainId: COSMOS_CHAIN_ID,
|
|
864
|
+
rpcUrls: [{ http: 'https://rpc.cosmos.invalid' }],
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
testLogger,
|
|
869
|
+
);
|
|
870
|
+
const quote = createTestQuote(
|
|
871
|
+
{ fromChainId: COSMOS_CHAIN_ID },
|
|
872
|
+
{ fromChain: COSMOS_CHAIN_ID },
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
await bridge.execute(quote, {
|
|
877
|
+
[ProtocolType.Cosmos]: TEST_PRIVATE_KEY,
|
|
878
|
+
});
|
|
879
|
+
expect.fail('Should have thrown for unsupported source protocol Cosmos');
|
|
880
|
+
} catch (error: unknown) {
|
|
881
|
+
const msg = (error as Error).message;
|
|
882
|
+
expect(msg).to.include("Unsupported protocol type 'cosmos'");
|
|
883
|
+
}
|
|
884
|
+
});
|
|
615
885
|
});
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
} from '@lifi/sdk';
|
|
15
15
|
import bs58 from 'bs58';
|
|
16
16
|
import type { ChainMetadata } from '@hyperlane-xyz/sdk';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ProtocolType,
|
|
19
|
+
assert,
|
|
20
|
+
ensure0x,
|
|
21
|
+
isEVMLike,
|
|
22
|
+
} from '@hyperlane-xyz/utils';
|
|
18
23
|
import type { Logger } from 'pino';
|
|
19
24
|
import { type Chain, createWalletClient, http } from 'viem';
|
|
20
25
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
@@ -133,21 +138,25 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
133
138
|
constructor(config: ExternalBridgeConfig, logger: Logger) {
|
|
134
139
|
this.config = config;
|
|
135
140
|
this.logger = logger;
|
|
136
|
-
// Build chainId -> metadata map for O(1) lookups
|
|
141
|
+
// Build LiFi chainId -> metadata map for O(1) lookups.
|
|
142
|
+
// Numeric chainIds are only unique for EVM chains; non-EVM chains can
|
|
143
|
+
// legitimately collide (e.g. radix and ethereum both use 1), so index
|
|
144
|
+
// non-EVM chains only through explicit Hyperlane-domain -> LiFi mappings.
|
|
137
145
|
this.chainMetadataByChainId = new Map();
|
|
138
146
|
if (config.chainMetadata) {
|
|
147
|
+
const metadataByDomainId = new Map<number, ChainMetadata>();
|
|
139
148
|
for (const metadata of Object.values(config.chainMetadata)) {
|
|
140
|
-
|
|
149
|
+
metadataByDomainId.set(metadata.domainId, metadata);
|
|
150
|
+
if (metadata.chainId !== undefined && isEVMLike(metadata.protocol)) {
|
|
141
151
|
this.chainMetadataByChainId.set(Number(metadata.chainId), metadata);
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
|
-
// Also key by LiFi chain IDs so both Hyperlane domains and LiFi IDs
|
|
154
|
+
// Also key by LiFi chain IDs so both Hyperlane domains and LiFi IDs
|
|
155
|
+
// resolve to the same metadata for non-EVM chains like Solana.
|
|
145
156
|
for (const [hyperlaneDomainId, lifiChainId] of Object.entries(
|
|
146
157
|
HYPERLANE_TO_LIFI_CHAIN_IDS,
|
|
147
158
|
)) {
|
|
148
|
-
const metadata =
|
|
149
|
-
Number(hyperlaneDomainId),
|
|
150
|
-
);
|
|
159
|
+
const metadata = metadataByDomainId.get(Number(hyperlaneDomainId));
|
|
151
160
|
if (metadata !== undefined) {
|
|
152
161
|
this.chainMetadataByChainId.set(Number(lifiChainId), metadata);
|
|
153
162
|
}
|
|
@@ -179,11 +188,24 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
179
188
|
* Iterates metadata to find matching chainId and returns first HTTP RPC URL.
|
|
180
189
|
*/
|
|
181
190
|
private getRpcUrlForChainId(chainId: number): string | undefined {
|
|
182
|
-
return this.
|
|
191
|
+
return this.getMetadataForChainId(chainId)?.rpcUrls?.[0]?.http;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
private getProtocolTypeForChainId(chainId: number): ProtocolType | undefined {
|
|
186
|
-
return this.
|
|
195
|
+
return this.getMetadataForChainId(chainId)?.protocol;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private getMetadataForChainId(chainId: number): ChainMetadata | undefined {
|
|
199
|
+
const directMetadata = this.chainMetadataByChainId.get(chainId);
|
|
200
|
+
if (directMetadata) {
|
|
201
|
+
return directMetadata;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const matches = Object.values(this.config.chainMetadata ?? {}).filter(
|
|
205
|
+
(metadata) =>
|
|
206
|
+
metadata.chainId !== undefined && Number(metadata.chainId) === chainId,
|
|
207
|
+
);
|
|
208
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
private addressesEqual(a: string, b: string, chainId: number): boolean {
|
|
@@ -196,58 +218,53 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
/**
|
|
199
|
-
* Configure LiFi SDK
|
|
200
|
-
* Sets up wallet/signer for each protocol type present in the keys map.
|
|
221
|
+
* Configure the LiFi SDK provider for the route source protocol.
|
|
201
222
|
*/
|
|
202
|
-
private
|
|
203
|
-
|
|
223
|
+
private configureLiFiProvider(
|
|
224
|
+
protocol: ProtocolType,
|
|
225
|
+
key: string,
|
|
204
226
|
fromChain: number,
|
|
205
227
|
fromRpcUrl: string | undefined,
|
|
206
228
|
): void {
|
|
207
229
|
const providers: Parameters<typeof lifiConfig.setProviders>[0] = [];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
getWalletAdapter: async () => new KeypairWalletAdapter(base58Key),
|
|
242
|
-
}),
|
|
243
|
-
);
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
default:
|
|
247
|
-
throw new Error(
|
|
248
|
-
`Unsupported protocol type '${protocol}' for LiFi provider`,
|
|
249
|
-
);
|
|
230
|
+
switch (protocol) {
|
|
231
|
+
case ProtocolType.Ethereum: {
|
|
232
|
+
const account = privateKeyToAccount(ensure0x(key) as `0x${string}`);
|
|
233
|
+
const chain = getViemChain(fromChain, fromRpcUrl);
|
|
234
|
+
const walletClient = createWalletClient({
|
|
235
|
+
account,
|
|
236
|
+
chain,
|
|
237
|
+
transport: http(fromRpcUrl),
|
|
238
|
+
});
|
|
239
|
+
providers.push(
|
|
240
|
+
EVM({
|
|
241
|
+
getWalletClient: async () => walletClient,
|
|
242
|
+
switchChain: async (requiredChainId: number) => {
|
|
243
|
+
const switchRpcUrl = this.getRpcUrlForChainId(requiredChainId);
|
|
244
|
+
const requiredChain = getViemChain(requiredChainId, switchRpcUrl);
|
|
245
|
+
return createWalletClient({
|
|
246
|
+
account,
|
|
247
|
+
chain: requiredChain,
|
|
248
|
+
transport: http(switchRpcUrl),
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case ProtocolType.Sealevel: {
|
|
256
|
+
const base58Key = toBase58SolanaKey(key);
|
|
257
|
+
providers.push(
|
|
258
|
+
Solana({
|
|
259
|
+
getWalletAdapter: async () => new KeypairWalletAdapter(base58Key),
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
break;
|
|
250
263
|
}
|
|
264
|
+
default:
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Unsupported protocol type '${protocol}' for LiFi provider`,
|
|
267
|
+
);
|
|
251
268
|
}
|
|
252
269
|
|
|
253
270
|
lifiConfig.setProviders(providers);
|
|
@@ -255,9 +272,9 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
255
272
|
this.logger.debug(
|
|
256
273
|
{
|
|
257
274
|
fromChain,
|
|
258
|
-
|
|
275
|
+
protocol,
|
|
259
276
|
},
|
|
260
|
-
'Configured LiFi
|
|
277
|
+
'Configured LiFi provider for route execution',
|
|
261
278
|
);
|
|
262
279
|
}
|
|
263
280
|
|
|
@@ -381,7 +398,7 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
381
398
|
slippage: (params.slippage ?? this.config.defaultSlippage ?? 0.005)
|
|
382
399
|
.toFixed(4)
|
|
383
400
|
.replace(/\.?0+$/, ''),
|
|
384
|
-
order: '
|
|
401
|
+
order: 'RECOMMENDED',
|
|
385
402
|
integrator: this.config.integrator,
|
|
386
403
|
});
|
|
387
404
|
|
|
@@ -491,9 +508,10 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
491
508
|
const fromChain = route.fromChainId;
|
|
492
509
|
const toChain = route.toChainId;
|
|
493
510
|
const fromProtocol = this.getProtocolTypeForChainId(fromChain);
|
|
511
|
+
const sourceProtocol = fromProtocol ?? ProtocolType.Ethereum;
|
|
494
512
|
assert(
|
|
495
|
-
privateKeys[
|
|
496
|
-
`Missing private key for source chain protocol ${
|
|
513
|
+
privateKeys[sourceProtocol],
|
|
514
|
+
`Missing private key for source chain protocol ${sourceProtocol}`,
|
|
497
515
|
);
|
|
498
516
|
|
|
499
517
|
this.logger.info(
|
|
@@ -521,14 +539,11 @@ export class LiFiBridge implements IExternalBridge {
|
|
|
521
539
|
let executedRoute!: RouteExtended;
|
|
522
540
|
|
|
523
541
|
try {
|
|
524
|
-
this.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
protocols: Object.keys(privateKeys),
|
|
530
|
-
},
|
|
531
|
-
'Configured LiFi providers for route execution',
|
|
542
|
+
this.configureLiFiProvider(
|
|
543
|
+
sourceProtocol,
|
|
544
|
+
privateKeys[sourceProtocol]!,
|
|
545
|
+
fromChain,
|
|
546
|
+
fromRpcUrl,
|
|
532
547
|
);
|
|
533
548
|
|
|
534
549
|
// Execute route with update callbacks
|
|
@@ -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
|
}
|