@avalabs/bridge-unified 2.0.1 → 2.1.1
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +7 -6
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bridges/cctp/__mocks__/chain.mocks.ts +2 -0
- package/src/bridges/cctp/factory.ts +4 -0
- package/src/bridges/cctp/handlers/estimate-gas.test.ts +110 -0
- package/src/bridges/cctp/handlers/estimate-gas.ts +58 -0
- package/src/bridges/cctp/handlers/transfer-asset.ts +10 -10
- package/src/bridges/cctp/types/chain.ts +5 -0
- package/src/bridges/cctp/types/config.ts +4 -3
- package/src/bridges/cctp/utils/build-tx.ts +30 -0
- package/src/types/bridge.ts +1 -0
- package/src/unified-bridge-service.test.ts +1 -0
- package/src/unified-bridge-service.ts +7 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getClientForChain } from '../../../utils/client';
|
|
2
|
+
import { BridgeType, type BridgeService, type TransferParams } from '../../../types';
|
|
3
|
+
|
|
4
|
+
import { CCTP_CONFIG } from '../__mocks__/config.mock';
|
|
5
|
+
import { BRIDGE_ASSET } from '../__mocks__/asset.mock';
|
|
6
|
+
import { SOURCE_CHAIN, TARGET_CHAIN } from '../__mocks__/chain.mocks';
|
|
7
|
+
|
|
8
|
+
import { estimateGas } from './estimate-gas';
|
|
9
|
+
import { isAddress } from 'viem';
|
|
10
|
+
|
|
11
|
+
jest.mock('viem');
|
|
12
|
+
jest.mock('../../../utils/client');
|
|
13
|
+
|
|
14
|
+
describe('CCTP estimateGas', () => {
|
|
15
|
+
const sourceProviderMock = {
|
|
16
|
+
request: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
const targetProviderMock = {
|
|
19
|
+
request: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
const bridgeMock = {
|
|
22
|
+
type: BridgeType.CCTP,
|
|
23
|
+
config: CCTP_CONFIG,
|
|
24
|
+
ensureHasConfig: jest.fn(),
|
|
25
|
+
estimateGas: jest.fn(),
|
|
26
|
+
} as unknown as BridgeService;
|
|
27
|
+
|
|
28
|
+
const getTransferParams = (props?: Partial<TransferParams>) =>
|
|
29
|
+
({
|
|
30
|
+
asset: BRIDGE_ASSET,
|
|
31
|
+
amount: 1_000_000,
|
|
32
|
+
fromAddress: '0x787',
|
|
33
|
+
sourceChain: SOURCE_CHAIN,
|
|
34
|
+
targetChain: TARGET_CHAIN,
|
|
35
|
+
sourceProvider: sourceProviderMock,
|
|
36
|
+
targetProvider: targetProviderMock,
|
|
37
|
+
...(props ?? {}),
|
|
38
|
+
}) as TransferParams;
|
|
39
|
+
|
|
40
|
+
const clientMock = {
|
|
41
|
+
readContract: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.resetAllMocks();
|
|
46
|
+
(getClientForChain as jest.Mock).mockReturnValue(clientMock);
|
|
47
|
+
jest.mocked(isAddress).mockReturnValue(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('calls ensureHasConfig', async () => {
|
|
51
|
+
const error = new Error('error');
|
|
52
|
+
const params = getTransferParams();
|
|
53
|
+
(bridgeMock.ensureHasConfig as jest.Mock).mockRejectedValueOnce(error);
|
|
54
|
+
await expect(estimateGas(bridgeMock, params)).rejects.toThrow(error);
|
|
55
|
+
|
|
56
|
+
expect(bridgeMock.ensureHasConfig).toHaveBeenCalled();
|
|
57
|
+
expect(getClientForChain).not.toHaveBeenCalled();
|
|
58
|
+
expect(clientMock.readContract).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('when allowance approval is required', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
clientMock.readContract.mockResolvedValue(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('when onboarding', () => {
|
|
67
|
+
const params = getTransferParams();
|
|
68
|
+
|
|
69
|
+
it('returns the sum of approval and transfer gas estimates', async () => {
|
|
70
|
+
expect(await estimateGas(bridgeMock, params)).toEqual(235_000n);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('when offboarding', () => {
|
|
75
|
+
const params = getTransferParams({
|
|
76
|
+
sourceChain: TARGET_CHAIN,
|
|
77
|
+
targetChain: SOURCE_CHAIN,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns the sum of approval and transfer gas estimates', async () => {
|
|
81
|
+
expect(await estimateGas(bridgeMock, params)).toEqual(275_000n);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('when allowance approval is NOT required', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
clientMock.readContract.mockResolvedValue(2_000_000n);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('when onboarding', () => {
|
|
92
|
+
const params = getTransferParams();
|
|
93
|
+
|
|
94
|
+
it('returns the gas estimate for transfer tx alone', async () => {
|
|
95
|
+
expect(await estimateGas(bridgeMock, params)).toEqual(175_000n);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('when offboarding', () => {
|
|
100
|
+
const params = getTransferParams({
|
|
101
|
+
sourceChain: TARGET_CHAIN,
|
|
102
|
+
targetChain: SOURCE_CHAIN,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns the gas estimate for transfer tx alone', async () => {
|
|
106
|
+
expect(await estimateGas(bridgeMock, params)).toEqual(215_000n);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isAddress } from 'viem';
|
|
2
|
+
|
|
3
|
+
import { InvalidParamsError } from '../../../errors';
|
|
4
|
+
import { getClientForChain } from '../../../utils/client';
|
|
5
|
+
import { ErrorReason, type BridgeService, type TransferParams } from '../../../types';
|
|
6
|
+
|
|
7
|
+
import { ERC20_ABI } from '../abis/erc20';
|
|
8
|
+
import { getTransferData } from '../utils/transfer-data';
|
|
9
|
+
import { ChainDomain } from '../types/chain';
|
|
10
|
+
|
|
11
|
+
const ETH_APPROVAL_TX_GAS_ESTIMATE = 60_000n; // 55.5k gas on average (+/- 200 units)
|
|
12
|
+
const ETH_TRANSFER_TX_GAS_ESTIMATE = 175_000n; // 161.5k gas on average (+/- 200 units)
|
|
13
|
+
const AVAX_APPROVAL_TX_GAS_ESTIMATE = 60_000n; // 55.5k gas on average (+/- 200 units)
|
|
14
|
+
const AVAX_TRANSFER_TX_GAS_ESTIMATE = 215_000n; // 203k gas on average (+/- 200 units)
|
|
15
|
+
/**
|
|
16
|
+
* The CCTP bridging consists of up to two transactions:
|
|
17
|
+
*
|
|
18
|
+
* 1. Token spend approval (technically optional, but realistically performed basically every time)
|
|
19
|
+
* 2. Token transfer (required)
|
|
20
|
+
*
|
|
21
|
+
* Since the 2nd one needs the first transaction to be complete, calling .estimateGas() is not possible.
|
|
22
|
+
* The RPC call raises an error since it cannot execute the transfer transaction locally without
|
|
23
|
+
* the allowance being set. For that reason, we're using hard-coded estimates here, with small buffers added.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: These estimates are only supposed to be used to approximate the network fees in the UI.
|
|
26
|
+
* DO NOT use them as `gasLimit` prop on the transactions!
|
|
27
|
+
*/
|
|
28
|
+
export async function estimateGas(bridge: BridgeService, params: TransferParams): Promise<bigint> {
|
|
29
|
+
await bridge.ensureHasConfig();
|
|
30
|
+
|
|
31
|
+
const { sourceChain, targetChain, asset, amount, fromAddress, toAddress: maybeToAddress, sourceProvider } = params;
|
|
32
|
+
const toAddress = maybeToAddress ?? fromAddress;
|
|
33
|
+
|
|
34
|
+
if (!isAddress(fromAddress) || !isAddress(toAddress)) {
|
|
35
|
+
throw new InvalidParamsError(ErrorReason.INCORRECT_ADDRESS_PROVIDED);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { sourceChainData, burnToken } = getTransferData({ sourceChain, targetChain, asset, amount }, bridge.config!);
|
|
39
|
+
|
|
40
|
+
const client = getClientForChain({ chain: sourceChain, provider: sourceProvider });
|
|
41
|
+
|
|
42
|
+
const allowance = await client.readContract({
|
|
43
|
+
address: burnToken.address,
|
|
44
|
+
abi: ERC20_ABI,
|
|
45
|
+
functionName: 'allowance',
|
|
46
|
+
args: [fromAddress, sourceChainData.tokenRouterAddress],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const isOffboarding = sourceChainData.domain === ChainDomain.Avalanche;
|
|
50
|
+
|
|
51
|
+
if (allowance >= amount) {
|
|
52
|
+
return isOffboarding ? AVAX_TRANSFER_TX_GAS_ESTIMATE : ETH_TRANSFER_TX_GAS_ESTIMATE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return isOffboarding
|
|
56
|
+
? AVAX_APPROVAL_TX_GAS_ESTIMATE + AVAX_TRANSFER_TX_GAS_ESTIMATE
|
|
57
|
+
: ETH_APPROVAL_TX_GAS_ESTIMATE + ETH_TRANSFER_TX_GAS_ESTIMATE;
|
|
58
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isAddress, type PublicClient } from 'viem';
|
|
2
2
|
import {
|
|
3
3
|
ErrorReason,
|
|
4
4
|
type BridgeService,
|
|
@@ -13,6 +13,7 @@ import { ERC20_ABI } from '../abis/erc20';
|
|
|
13
13
|
import { getTransferData } from '../utils/transfer-data';
|
|
14
14
|
import { TOKEN_ROUTER_ABI } from '../abis/token-router';
|
|
15
15
|
import { InvalidParamsError } from '../../../errors';
|
|
16
|
+
import { buildApprovalTxData, buildTransferTxData } from '../utils/build-tx';
|
|
16
17
|
|
|
17
18
|
const approveAndTransfer = async (bridge: BridgeService, params: TransferParams) => {
|
|
18
19
|
const {
|
|
@@ -56,12 +57,10 @@ const approveAndTransfer = async (bridge: BridgeService, params: TransferParams)
|
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
if (sign) {
|
|
59
|
-
const data =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
args: [sourceChainData.tokenRouterAddress, amount],
|
|
60
|
+
const data = buildApprovalTxData({
|
|
61
|
+
amount,
|
|
62
|
+
sourceChainData,
|
|
63
63
|
});
|
|
64
|
-
|
|
65
64
|
const txHash = await sign(
|
|
66
65
|
{
|
|
67
66
|
from: fromAddress,
|
|
@@ -93,10 +92,11 @@ const approveAndTransfer = async (bridge: BridgeService, params: TransferParams)
|
|
|
93
92
|
});
|
|
94
93
|
|
|
95
94
|
if (sign) {
|
|
96
|
-
const data =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const data = buildTransferTxData({
|
|
96
|
+
amount,
|
|
97
|
+
burnToken,
|
|
98
|
+
targetChainData,
|
|
99
|
+
toAddress,
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
return sign(
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { Address } from 'viem';
|
|
2
|
+
import type { ChainDomain } from './chain';
|
|
2
3
|
|
|
3
|
-
type Token = {
|
|
4
|
+
export type Token = {
|
|
4
5
|
address: Address;
|
|
5
6
|
name: string;
|
|
6
7
|
symbol: string;
|
|
7
8
|
decimals: number;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
type ChainData = {
|
|
11
|
+
export type ChainData = {
|
|
11
12
|
chainId: string;
|
|
12
|
-
domain:
|
|
13
|
+
domain: ChainDomain;
|
|
13
14
|
tokenRouterAddress: Address;
|
|
14
15
|
messageTransmitterAddress: Address;
|
|
15
16
|
tokens: Token[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { encodeFunctionData } from 'viem';
|
|
2
|
+
import type { ChainData, Token } from '../types/config';
|
|
3
|
+
import { TOKEN_ROUTER_ABI } from '../abis/token-router';
|
|
4
|
+
import { ERC20_ABI } from '../abis/erc20';
|
|
5
|
+
|
|
6
|
+
export function buildTransferTxData({
|
|
7
|
+
amount,
|
|
8
|
+
burnToken,
|
|
9
|
+
targetChainData,
|
|
10
|
+
toAddress,
|
|
11
|
+
}: {
|
|
12
|
+
targetChainData: ChainData;
|
|
13
|
+
toAddress: `0x${string}`;
|
|
14
|
+
burnToken: Token;
|
|
15
|
+
amount: bigint;
|
|
16
|
+
}) {
|
|
17
|
+
return encodeFunctionData({
|
|
18
|
+
abi: TOKEN_ROUTER_ABI,
|
|
19
|
+
functionName: 'transferTokens',
|
|
20
|
+
args: [amount, targetChainData.domain, toAddress, burnToken.address],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildApprovalTxData({ amount, sourceChainData }: { amount: bigint; sourceChainData: ChainData }) {
|
|
25
|
+
return encodeFunctionData({
|
|
26
|
+
abi: ERC20_ABI,
|
|
27
|
+
functionName: 'approve',
|
|
28
|
+
args: [sourceChainData.tokenRouterAddress, amount],
|
|
29
|
+
});
|
|
30
|
+
}
|
package/src/types/bridge.ts
CHANGED
|
@@ -54,6 +54,7 @@ export type BridgeService = {
|
|
|
54
54
|
config: BridgeConfig | null;
|
|
55
55
|
ensureHasConfig: () => Promise<void>;
|
|
56
56
|
updateConfig: () => Promise<void>;
|
|
57
|
+
estimateGas: (params: TransferParams) => Promise<bigint>;
|
|
57
58
|
getAssets: () => Promise<ChainAssetMap>;
|
|
58
59
|
getFees: (params: FeeParams) => Promise<AssetFeeMap>;
|
|
59
60
|
transferAsset: (params: TransferParams) => Promise<BridgeTransfer>;
|
|
@@ -48,6 +48,7 @@ describe('unified-bridge-service', () => {
|
|
|
48
48
|
bridges: enabledBridges,
|
|
49
49
|
init: expect.any(Function),
|
|
50
50
|
updateConfigs: expect.any(Function),
|
|
51
|
+
estimateGas: expect.any(Function),
|
|
51
52
|
getAssets: expect.any(Function),
|
|
52
53
|
getFees: expect.any(Function),
|
|
53
54
|
transferAsset: expect.any(Function),
|
|
@@ -76,6 +76,12 @@ export const createUnifiedBridgeService = ({ environment, disabledBridgeTypes }:
|
|
|
76
76
|
return bridge.trackTransfer(params);
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
const estimateGas = async (params: TransferParams) => {
|
|
80
|
+
const { bridge } = getBridgeForTransfer(enabledBridgeServices, params.asset, params.targetChain.chainId);
|
|
81
|
+
|
|
82
|
+
return bridge.estimateGas(params);
|
|
83
|
+
};
|
|
84
|
+
|
|
79
85
|
return {
|
|
80
86
|
environment,
|
|
81
87
|
bridges: enabledBridgeServices,
|
|
@@ -83,6 +89,7 @@ export const createUnifiedBridgeService = ({ environment, disabledBridgeTypes }:
|
|
|
83
89
|
updateConfigs,
|
|
84
90
|
getAssets,
|
|
85
91
|
getFees,
|
|
92
|
+
estimateGas,
|
|
86
93
|
canTransferAsset,
|
|
87
94
|
transferAsset,
|
|
88
95
|
trackTransfer,
|