@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
|
@@ -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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;AAE7C,QAAQ,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACvE,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,SAAS,cAAc,CAAC,QAAsB;QAC5C,MAAM,WAAW,GAAG;YAClB,sBAAsB,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC;gBAC5C,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;gBAC3B,aAAa,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE;aAClD,CAAC;YACF,wBAAwB,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC;gBAC9C,EAAE,EAAE,UAAU;gBACd,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,KAAK;aACb,CAAC;SACH,CAAC;QAEF,MAAM,SAAS,GAAG;YAChB,QAAQ,EAAE,aAAa,CAAC,YAAY,EAAE,yCAAyC;YAC/E,aAAa,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC;SAC7B,CAAC;QAEtB,MAAM,aAAa,GAAG;YACpB,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;YACxC,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;YAC3C,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC;gBAChC,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC1D,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC;oBAChC,YAAY,EAAE,SAAS,CAAC,IAAI,CAAC,YAAe,CAAC;oBAC7C,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,YAAe,CAAC;iBAC1C,CAAC;aACH,CAAC;SACyB,CAAC;QAE9B,MAAM,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzD,MAAM,qBAAqB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAEzD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,CAAC;IACpE,CAAC;IAED,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,GAC9D,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAEpC,MAAM,MAAM,GAAG,MAAM,sBAAsB,CACzC,MAAmB,EACnB,UAAuB,EACvB,qBAAqB,EAAE,mBAAmB;QAC1C,oBAAoB,EAAE,kBAAkB;QACxC,aAAa,EACb,EAAS,EAAE,wBAAwB;QACnC,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,CACX,CAAC;QAEF,gEAAgE;QAChE,MAAM,CAAC,MAAM,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,eAAe,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,GAC9D,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,MAAM,sBAAsB,CACzC,QAAqB,EACrB,UAAuB,EACvB,aAAa,EACb,aAAa,EACb,aAAa,EACb,EAAS,EACT,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,CACX,CAAC;QAEF,4DAA4D;QAC5D,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -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;
|
|
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"}
|
package/dist/utils/tokenUtils.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
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.
|
|
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.
|
|
47
|
-
"@hyperlane-xyz/provider-sdk": "
|
|
48
|
-
"@hyperlane-xyz/sdk": "33.0
|
|
49
|
-
"@hyperlane-xyz/utils": "33.0
|
|
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.
|
|
70
|
-
"@hyperlane-xyz/tsconfig": "^33.0
|
|
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 {
|
|
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
|
|
|
@@ -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
|