Skip to content

Commit 054b178

Browse files
authored
feat(rebalancer): add Tron protocol support (#8320)
1 parent 47649b7 commit 054b178

15 files changed

Lines changed: 916 additions & 114 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/rebalancer': minor
3+
---
4+
5+
Added Tron blockchain support to the rebalancer using ProtocolType.Tron. Tron chains were treated as EVM-like for signer creation, block tag resolution, gas estimation, and transaction receipt parsing. The LiFi bridge was updated to gracefully skip Tron chains as no Tron-compatible aggregator is available.

typescript/rebalancer/src/bridges/LiFiBridge.test.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const testLogger = pino({ level: 'silent' });
1515

1616
const TEST_PRIVATE_KEY =
1717
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
18+
const OTHER_PRIVATE_KEY = `${TEST_PRIVATE_KEY.slice(0, -1)}1`;
1819

1920
const BRIDGE_CONFIG: ExternalBridgeConfig = {
2021
integrator: 'test-rebalancer',
@@ -64,6 +65,28 @@ const DUPLICATE_CHAIN_ID_CONFIG: ExternalBridgeConfig = {
6465
},
6566
};
6667

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+
6790
// Use all-digit hex addresses to avoid EIP-55 checksum case mutations
6891
const TOKEN_ADDR = '0x1234567890123456789012345678901234567890';
6992
const SENDER_ADDR = '0x9876543210987654321098765432109876543210';
@@ -711,4 +734,152 @@ describe('LiFiBridge constructor chainMetadataByChainId', function () {
711734
expect(msg.toLowerCase()).to.include('private key');
712735
});
713736
});
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+
});
714885
});

typescript/rebalancer/src/bridges/LiFiBridge.ts

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414
} from '@lifi/sdk';
1515
import bs58 from 'bs58';
1616
import type { ChainMetadata } from '@hyperlane-xyz/sdk';
17-
import { ProtocolType, assert, ensure0x } from '@hyperlane-xyz/utils';
17+
import {
18+
ProtocolType,
19+
assert,
20+
ensure0x,
21+
isEVMLike,
22+
} from '@hyperlane-xyz/utils';
1823
import type { Logger } from 'pino';
1924
import { type Chain, createWalletClient, http } from 'viem';
2025
import { privateKeyToAccount } from 'viem/accounts';
@@ -142,10 +147,8 @@ export class LiFiBridge implements IExternalBridge {
142147
const metadataByDomainId = new Map<number, ChainMetadata>();
143148
for (const metadata of Object.values(config.chainMetadata)) {
144149
metadataByDomainId.set(metadata.domainId, metadata);
145-
if (metadata.chainId !== undefined) {
146-
if (metadata.protocol === ProtocolType.Ethereum) {
147-
this.chainMetadataByChainId.set(Number(metadata.chainId), metadata);
148-
}
150+
if (metadata.chainId !== undefined && isEVMLike(metadata.protocol)) {
151+
this.chainMetadataByChainId.set(Number(metadata.chainId), metadata);
149152
}
150153
}
151154
// Also key by LiFi chain IDs so both Hyperlane domains and LiFi IDs
@@ -185,11 +188,24 @@ export class LiFiBridge implements IExternalBridge {
185188
* Iterates metadata to find matching chainId and returns first HTTP RPC URL.
186189
*/
187190
private getRpcUrlForChainId(chainId: number): string | undefined {
188-
return this.chainMetadataByChainId.get(chainId)?.rpcUrls?.[0]?.http;
191+
return this.getMetadataForChainId(chainId)?.rpcUrls?.[0]?.http;
189192
}
190193

191194
private getProtocolTypeForChainId(chainId: number): ProtocolType | undefined {
192-
return this.chainMetadataByChainId.get(chainId)?.protocol;
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;
193209
}
194210

195211
private addressesEqual(a: string, b: string, chainId: number): boolean {
@@ -202,68 +218,63 @@ export class LiFiBridge implements IExternalBridge {
202218
}
203219

204220
/**
205-
* Configure LiFi SDK providers from the given private keys.
206-
* Sets up wallet/signer for each protocol type present in the keys map.
221+
* Configure the LiFi SDK provider for the route source protocol.
207222
*/
208-
private configureLiFiProviders(
209-
privateKeys: Partial<Record<ProtocolType, string>>,
223+
private configureLiFiProvider(
224+
protocol: ProtocolType,
225+
key: string,
210226
fromChain: number,
211227
fromRpcUrl: string | undefined,
212228
): void {
213229
const providers: Parameters<typeof lifiConfig.setProviders>[0] = [];
214-
for (const [protocol, key] of Object.entries(privateKeys)) {
215-
switch (protocol) {
216-
case ProtocolType.Ethereum: {
217-
const account = privateKeyToAccount(ensure0x(key) as `0x${string}`);
218-
const chain = getViemChain(fromChain, fromRpcUrl);
219-
const walletClient = createWalletClient({
220-
account,
221-
chain,
222-
transport: http(fromRpcUrl),
223-
});
224-
providers.push(
225-
EVM({
226-
getWalletClient: async () => walletClient,
227-
switchChain: async (requiredChainId: number) => {
228-
const switchRpcUrl = this.getRpcUrlForChainId(requiredChainId);
229-
const requiredChain = getViemChain(
230-
requiredChainId,
231-
switchRpcUrl,
232-
);
233-
return createWalletClient({
234-
account,
235-
chain: requiredChain,
236-
transport: http(switchRpcUrl),
237-
});
238-
},
239-
}),
240-
);
241-
break;
242-
}
243-
case ProtocolType.Sealevel: {
244-
const base58Key = toBase58SolanaKey(key);
245-
providers.push(
246-
Solana({
247-
getWalletAdapter: async () => new KeypairWalletAdapter(base58Key),
248-
}),
249-
);
250-
break;
251-
}
252-
default:
253-
throw new Error(
254-
`Unsupported protocol type '${protocol}' for LiFi provider`,
255-
);
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;
256254
}
255+
case ProtocolType.Sealevel: {
256+
const base58Key = toBase58SolanaKey(key);
257+
providers.push(
258+
Solana({
259+
getWalletAdapter: async () => new KeypairWalletAdapter(base58Key),
260+
}),
261+
);
262+
break;
263+
}
264+
default:
265+
throw new Error(
266+
`Unsupported protocol type '${protocol}' for LiFi provider`,
267+
);
257268
}
258269

259270
lifiConfig.setProviders(providers);
260271

261272
this.logger.debug(
262273
{
263274
fromChain,
264-
protocols: Object.keys(privateKeys),
275+
protocol,
265276
},
266-
'Configured LiFi providers for route execution',
277+
'Configured LiFi provider for route execution',
267278
);
268279
}
269280

@@ -497,9 +508,10 @@ export class LiFiBridge implements IExternalBridge {
497508
const fromChain = route.fromChainId;
498509
const toChain = route.toChainId;
499510
const fromProtocol = this.getProtocolTypeForChainId(fromChain);
511+
const sourceProtocol = fromProtocol ?? ProtocolType.Ethereum;
500512
assert(
501-
privateKeys[fromProtocol ?? ProtocolType.Ethereum],
502-
`Missing private key for source chain protocol ${fromProtocol ?? ProtocolType.Ethereum}`,
513+
privateKeys[sourceProtocol],
514+
`Missing private key for source chain protocol ${sourceProtocol}`,
503515
);
504516

505517
this.logger.info(
@@ -527,14 +539,11 @@ export class LiFiBridge implements IExternalBridge {
527539
let executedRoute!: RouteExtended;
528540

529541
try {
530-
this.configureLiFiProviders(privateKeys, fromChain, fromRpcUrl);
531-
532-
this.logger.debug(
533-
{
534-
fromChain,
535-
protocols: Object.keys(privateKeys),
536-
},
537-
'Configured LiFi providers for route execution',
542+
this.configureLiFiProvider(
543+
sourceProtocol,
544+
privateKeys[sourceProtocol]!,
545+
fromChain,
546+
fromRpcUrl,
538547
);
539548

540549
// Execute route with update callbacks

0 commit comments

Comments
 (0)