@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.
Files changed (54) hide show
  1. package/dist/bridges/LiFiBridge.d.ts +3 -3
  2. package/dist/bridges/LiFiBridge.d.ts.map +1 -1
  3. package/dist/bridges/LiFiBridge.js +59 -51
  4. package/dist/bridges/LiFiBridge.js.map +1 -1
  5. package/dist/bridges/LiFiBridge.test.js +176 -0
  6. package/dist/bridges/LiFiBridge.test.js.map +1 -1
  7. package/dist/config/RebalancerConfig.test.js +123 -0
  8. package/dist/config/RebalancerConfig.test.js.map +1 -1
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +9 -0
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/InventoryRebalancer.d.ts.map +1 -1
  13. package/dist/core/InventoryRebalancer.js +3 -2
  14. package/dist/core/InventoryRebalancer.js.map +1 -1
  15. package/dist/core/InventoryRebalancer.test.js +108 -1
  16. package/dist/core/InventoryRebalancer.test.js.map +1 -1
  17. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  18. package/dist/factories/RebalancerContextFactory.js +34 -24
  19. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  20. package/dist/factories/RebalancerContextFactory.test.js +84 -1
  21. package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
  22. package/dist/service.js +7 -4
  23. package/dist/service.js.map +1 -1
  24. package/dist/utils/blockTag.d.ts.map +1 -1
  25. package/dist/utils/blockTag.js +8 -3
  26. package/dist/utils/blockTag.js.map +1 -1
  27. package/dist/utils/blockTag.test.d.ts +2 -0
  28. package/dist/utils/blockTag.test.d.ts.map +1 -0
  29. package/dist/utils/blockTag.test.js +57 -0
  30. package/dist/utils/blockTag.test.js.map +1 -0
  31. package/dist/utils/gasEstimation.js +4 -4
  32. package/dist/utils/gasEstimation.js.map +1 -1
  33. package/dist/utils/gasEstimation.test.d.ts +2 -0
  34. package/dist/utils/gasEstimation.test.d.ts.map +1 -0
  35. package/dist/utils/gasEstimation.test.js +63 -0
  36. package/dist/utils/gasEstimation.test.js.map +1 -0
  37. package/dist/utils/tokenUtils.d.ts.map +1 -1
  38. package/dist/utils/tokenUtils.js +5 -2
  39. package/dist/utils/tokenUtils.js.map +1 -1
  40. package/package.json +7 -7
  41. package/src/bridges/LiFiBridge.test.ts +227 -0
  42. package/src/bridges/LiFiBridge.ts +82 -67
  43. package/src/config/RebalancerConfig.test.ts +135 -0
  44. package/src/config/types.ts +8 -0
  45. package/src/core/InventoryRebalancer.test.ts +160 -0
  46. package/src/core/InventoryRebalancer.ts +9 -3
  47. package/src/factories/RebalancerContextFactory.test.ts +116 -1
  48. package/src/factories/RebalancerContextFactory.ts +38 -28
  49. package/src/service.ts +11 -8
  50. package/src/utils/blockTag.test.ts +70 -0
  51. package/src/utils/blockTag.ts +11 -3
  52. package/src/utils/gasEstimation.test.ts +99 -0
  53. package/src/utils/gasEstimation.ts +4 -4
  54. package/src/utils/tokenUtils.ts +5 -2
@@ -0,0 +1,63 @@
1
+ import { expect } from 'chai';
2
+ import { BigNumber } from 'ethers';
3
+ import { pino } from 'pino';
4
+ import Sinon from 'sinon';
5
+ import { TokenStandard } from '@hyperlane-xyz/sdk';
6
+ import { ProtocolType } from '@hyperlane-xyz/utils';
7
+ import { calculateTransferCosts } from './gasEstimation.js';
8
+ const testLogger = pino({ level: 'silent' });
9
+ describe('calculateTransferCosts — Tron vs Sealevel protocol path', () => {
10
+ afterEach(() => {
11
+ Sinon.restore();
12
+ });
13
+ function createMockDeps(protocol) {
14
+ const mockAdapter = {
15
+ quoteTransferRemoteGas: Sinon.stub().resolves({
16
+ igpQuote: { amount: 1000n },
17
+ tokenFeeQuote: { amount: 0n, addressOrDenom: '' },
18
+ }),
19
+ populateTransferRemoteTx: Sinon.stub().resolves({
20
+ to: '0xRouter',
21
+ data: '0x',
22
+ value: 1000n,
23
+ }),
24
+ };
25
+ const mockToken = {
26
+ standard: TokenStandard.EvmHypNative, // Native so we reach the isEVMLike check
27
+ getHypAdapter: Sinon.stub().returns(mockAdapter),
28
+ };
29
+ const multiProvider = {
30
+ getDomainId: Sinon.stub().returns(42161),
31
+ getProtocol: Sinon.stub().returns(protocol),
32
+ getProvider: Sinon.stub().returns({
33
+ estimateGas: Sinon.stub().resolves(BigNumber.from(200000)),
34
+ getFeeData: Sinon.stub().resolves({
35
+ maxFeePerGas: BigNumber.from(10000000000n),
36
+ gasPrice: BigNumber.from(10000000000n),
37
+ }),
38
+ }),
39
+ };
40
+ const getTokenForChain = Sinon.stub().returns(mockToken);
41
+ const isNativeTokenStandard = Sinon.stub().returns(true);
42
+ return { multiProvider, getTokenForChain, isNativeTokenStandard };
43
+ }
44
+ it('Tron origin (EVM-like) produces non-zero gasCost for native tokens', async () => {
45
+ const { multiProvider, getTokenForChain, isNativeTokenStandard } = createMockDeps(ProtocolType.Tron);
46
+ const result = await calculateTransferCosts('tron', 'arbitrum', 10000000000000000000n, // 10 ETH available
47
+ 1000000000000000000n, // 1 ETH requested
48
+ multiProvider, {}, // warpCoreMultiProvider
49
+ getTokenForChain, '0xInventorySigner', isNativeTokenStandard, testLogger);
50
+ // Tron is EVM-like — gas estimation runs, producing gasCost > 0
51
+ expect(result.gasCost > 0n).to.be.true;
52
+ expect(result.igpCost).to.equal(1000n);
53
+ expect(result.maxTransferable > 0n).to.be.true;
54
+ });
55
+ it('Sealevel origin (non-EVM) returns gasCost = 0 for native tokens', async () => {
56
+ const { multiProvider, getTokenForChain, isNativeTokenStandard } = createMockDeps(ProtocolType.Sealevel);
57
+ const result = await calculateTransferCosts('solana', 'arbitrum', 200000000000n, 100000000000n, multiProvider, {}, getTokenForChain, '0xInventorySigner', isNativeTokenStandard, testLogger);
58
+ // Sealevel is non-EVM — gasCost is 0 (skips gas estimation)
59
+ expect(result.gasCost).to.equal(0n);
60
+ expect(result.igpCost).to.equal(1000n);
61
+ });
62
+ });
63
+ //# sourceMappingURL=gasEstimation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gasEstimation.test.js","sourceRoot":"","sources":["../../src/utils/gasEstimation.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tokenUtils.d.ts","sourceRoot":"","sources":["../../src/utils/tokenUtils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAEL,KAAK,KAAK,EACV,aAAa,EACd,MAAM,oBAAoB,CAAC;AAkB5B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2CAA2C,CACzD,KAAK,EAAE,KAAK,GACX,OAAO,CAKT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,KAAK,EACZ,kBAAkB,EAAE,kBAAkB,EACtC,qBAAqB,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,GAC1D,MAAM,CAaR"}
1
+ {"version":3,"file":"tokenUtils.d.ts","sourceRoot":"","sources":["../../src/utils/tokenUtils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAEL,KAAK,KAAK,EACV,aAAa,EACd,MAAM,oBAAoB,CAAC;AAqB5B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2CAA2C,CACzD,KAAK,EAAE,KAAK,GACX,OAAO,CAKT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,KAAK,EACZ,kBAAkB,EAAE,kBAAkB,EACtC,qBAAqB,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,GAC1D,MAAM,CAaR"}
@@ -5,14 +5,17 @@ const REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS = new Set([
5
5
  TokenStandard.EvmHypNative,
6
6
  TokenStandard.SealevelHypCollateral,
7
7
  TokenStandard.SealevelHypNative,
8
+ TokenStandard.TronHypCollateral,
9
+ TokenStandard.TronHypNative,
8
10
  ]);
9
- // SDK-backed native token standard check scoped to EVM and Sealevel only (2-protocol scope).
11
+ // SDK-backed native token standard check scoped to EVM, Sealevel, and Tron (3-protocol scope).
10
12
  // NOTE: We intentionally do NOT use the full PROTOCOL_TO_HYP_NATIVE_STANDARD map (all 7 protocols)
11
- // because the rebalancer only supports EVM and Sealevel native token bridging.
13
+ // because the rebalancer only supports EVM, Sealevel, and Tron native token bridging.
12
14
  // Expanding this would change gas reservation behavior for other protocols.
13
15
  const REBALANCER_NATIVE_STANDARDS = new Set([
14
16
  PROTOCOL_TO_HYP_NATIVE_STANDARD[ProtocolType.Ethereum],
15
17
  PROTOCOL_TO_HYP_NATIVE_STANDARD[ProtocolType.Sealevel],
18
+ PROTOCOL_TO_HYP_NATIVE_STANDARD[ProtocolType.Tron],
16
19
  ]);
17
20
  /**
18
21
  * Check if a token's balance is the same as native gas balance.
@@ -1 +1 @@
1
- {"version":3,"file":"tokenUtils.js","sourceRoot":"","sources":["../../src/utils/tokenUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI5D,OAAO,EACL,+BAA+B,EAE/B,aAAa,GACd,MAAM,oBAAoB,CAAC;AAE5B,MAAM,4CAA4C,GAAG,IAAI,GAAG,CAAgB;IAC1E,aAAa,CAAC,gBAAgB;IAC9B,aAAa,CAAC,YAAY;IAC1B,aAAa,CAAC,qBAAqB;IACnC,aAAa,CAAC,iBAAiB;CAChC,CAAC,CAAC;AAEH,6FAA6F;AAC7F,mGAAmG;AACnG,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC;IAC1C,+BAA+B,CAAC,YAAY,CAAC,QAAQ,CAAC;IACtD,+BAA+B,CAAC,YAAY,CAAC,QAAQ,CAAC;CACvD,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAuB;IAC3D,OAAO,2BAA2B,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2CAA2C,CACzD,KAAY;IAEZ,OAAO,CACL,KAAK,CAAC,gBAAgB,EAAE;QACxB,4CAA4C,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CACjE,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,6BAA6B,CAC3C,KAAY,EACZ,kBAAsC,EACtC,qBAA2D;IAE3D,IAAI,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,OAAO,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,KAAK,CAAC,wBAAwB;QAAE,OAAO,KAAK,CAAC,wBAAwB,CAAC;IAE1E,MAAM,CACJ,CAAC,4CAA4C,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EACjE,gEAAgE,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,QAAQ,GAAG,CACtG,CAAC;IAEF,OAAO,KAAK,CAAC,cAAc,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"tokenUtils.js","sourceRoot":"","sources":["../../src/utils/tokenUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI5D,OAAO,EACL,+BAA+B,EAE/B,aAAa,GACd,MAAM,oBAAoB,CAAC;AAE5B,MAAM,4CAA4C,GAAG,IAAI,GAAG,CAAgB;IAC1E,aAAa,CAAC,gBAAgB;IAC9B,aAAa,CAAC,YAAY;IAC1B,aAAa,CAAC,qBAAqB;IACnC,aAAa,CAAC,iBAAiB;IAC/B,aAAa,CAAC,iBAAiB;IAC/B,aAAa,CAAC,aAAa;CAC5B,CAAC,CAAC;AAEH,+FAA+F;AAC/F,mGAAmG;AACnG,sFAAsF;AACtF,4EAA4E;AAC5E,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC;IAC1C,+BAA+B,CAAC,YAAY,CAAC,QAAQ,CAAC;IACtD,+BAA+B,CAAC,YAAY,CAAC,QAAQ,CAAC;IACtD,+BAA+B,CAAC,YAAY,CAAC,IAAI,CAAC;CACnD,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAuB;IAC3D,OAAO,2BAA2B,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2CAA2C,CACzD,KAAY;IAEZ,OAAO,CACL,KAAK,CAAC,gBAAgB,EAAE;QACxB,4CAA4C,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CACjE,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,6BAA6B,CAC3C,KAAY,EACZ,kBAAsC,EACtC,qBAA2D;IAE3D,IAAI,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,OAAO,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,KAAK,CAAC,wBAAwB;QAAE,OAAO,KAAK,CAAC,wBAAwB,CAAC;IAE1E,MAAM,CACJ,CAAC,4CAA4C,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EACjE,gEAAgE,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,QAAQ,GAAG,CACtG,CAAC;IAEF,OAAO,KAAK,CAAC,cAAc,CAAC;AAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperlane-xyz/rebalancer",
3
- "version": "27.2.14",
3
+ "version": "27.3.0",
4
4
  "description": "Hyperlane Warp Route Collateral Rebalancer Service",
5
5
  "keywords": [
6
6
  "blockchain",
@@ -43,10 +43,10 @@
43
43
  "zod": "^3.21.2",
44
44
  "zod-validation-error": "^3.3.0",
45
45
  "@hyperlane-xyz/core": "11.3.1",
46
- "@hyperlane-xyz/metrics": "0.2.20",
47
- "@hyperlane-xyz/provider-sdk": "5.1.0",
48
- "@hyperlane-xyz/sdk": "33.0.2",
49
- "@hyperlane-xyz/utils": "33.0.2"
46
+ "@hyperlane-xyz/metrics": "0.2.21",
47
+ "@hyperlane-xyz/provider-sdk": "6.0.0",
48
+ "@hyperlane-xyz/sdk": "33.1.0",
49
+ "@hyperlane-xyz/utils": "33.1.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@ethersproject/constants": "*",
@@ -66,8 +66,8 @@
66
66
  "testcontainers": "11.12.0",
67
67
  "tsx": "^4.19.1",
68
68
  "typescript": "6.0.2",
69
- "@hyperlane-xyz/relayer": "1.1.27",
70
- "@hyperlane-xyz/tsconfig": "^33.0.2"
69
+ "@hyperlane-xyz/relayer": "1.1.28",
70
+ "@hyperlane-xyz/tsconfig": "^33.1.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=18"
@@ -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';
@@ -655,4 +708,178 @@ describe('LiFiBridge constructor chainMetadataByChainId', function () {
655
708
  expect(msg).to.include('does not match requested');
656
709
  }
657
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
+ });
658
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 { ProtocolType, assert, ensure0x } from '@hyperlane-xyz/utils';
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
- if (metadata.chainId !== undefined) {
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 resolve to the same metadata.
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 = this.chainMetadataByChainId.get(
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.chainMetadataByChainId.get(chainId)?.rpcUrls?.[0]?.http;
191
+ return this.getMetadataForChainId(chainId)?.rpcUrls?.[0]?.http;
183
192
  }
184
193
 
185
194
  private getProtocolTypeForChainId(chainId: number): ProtocolType | undefined {
186
- 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;
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 providers from the given private keys.
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 configureLiFiProviders(
203
- privateKeys: Partial<Record<ProtocolType, string>>,
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
- for (const [protocol, key] of Object.entries(privateKeys)) {
209
- switch (protocol) {
210
- case ProtocolType.Ethereum: {
211
- const account = privateKeyToAccount(ensure0x(key) as `0x${string}`);
212
- const chain = getViemChain(fromChain, fromRpcUrl);
213
- const walletClient = createWalletClient({
214
- account,
215
- chain,
216
- transport: http(fromRpcUrl),
217
- });
218
- providers.push(
219
- EVM({
220
- getWalletClient: async () => walletClient,
221
- switchChain: async (requiredChainId: number) => {
222
- const switchRpcUrl = this.getRpcUrlForChainId(requiredChainId);
223
- const requiredChain = getViemChain(
224
- requiredChainId,
225
- switchRpcUrl,
226
- );
227
- return createWalletClient({
228
- account,
229
- chain: requiredChain,
230
- transport: http(switchRpcUrl),
231
- });
232
- },
233
- }),
234
- );
235
- break;
236
- }
237
- case ProtocolType.Sealevel: {
238
- const base58Key = toBase58SolanaKey(key);
239
- providers.push(
240
- Solana({
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
- protocols: Object.keys(privateKeys),
275
+ protocol,
259
276
  },
260
- 'Configured LiFi providers for route execution',
277
+ 'Configured LiFi provider for route execution',
261
278
  );
262
279
  }
263
280
 
@@ -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[fromProtocol ?? ProtocolType.Ethereum],
496
- `Missing private key for source chain protocol ${fromProtocol ?? ProtocolType.Ethereum}`,
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.configureLiFiProviders(privateKeys, fromChain, fromRpcUrl);
525
-
526
- this.logger.debug(
527
- {
528
- fromChain,
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