@avalabs/bridge-unified 2.0.0 → 2.1.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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +6 -5
- package/CHANGELOG.md +12 -0
- package/dist/index.cjs +6 -6
- 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 +1 -1
- package/src/bridges/cctp/__mocks__/chain.mocks.ts +2 -0
- package/src/bridges/cctp/__mocks__/config.mock.ts +4 -1
- 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/track-transfer.test.ts +8 -4
- package/src/bridges/cctp/handlers/track-transfer.ts +1 -1
- 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
|
+
}
|
|
@@ -4,7 +4,11 @@ import { getClientForChain } from '../../../utils/client';
|
|
|
4
4
|
import { getNetworkFeeEVM } from '../../../utils/network-fee';
|
|
5
5
|
import { retryPromise } from '../../../utils/retry-promise';
|
|
6
6
|
import { getBridgeTrackinParams } from '../__mocks__/bridge-transfer.mock';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CCTP_CONFIG,
|
|
9
|
+
SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
|
|
10
|
+
TARGET_TRANSMITTER_ADDRESS,
|
|
11
|
+
} from '../__mocks__/config.mock';
|
|
8
12
|
import { getTrackingDelayByChainId } from '../utils/config';
|
|
9
13
|
import * as tracking from './track-transfer';
|
|
10
14
|
import {
|
|
@@ -301,7 +305,7 @@ describe('CCTP trackTransfer', () => {
|
|
|
301
305
|
const targetBlockNumber = 50n;
|
|
302
306
|
const receiptMock = {
|
|
303
307
|
status: 'succeeded',
|
|
304
|
-
logs: [{ address:
|
|
308
|
+
logs: [{ address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING }],
|
|
305
309
|
};
|
|
306
310
|
|
|
307
311
|
const params = getBridgeTrackinParams({
|
|
@@ -364,12 +368,12 @@ describe('CCTP trackTransfer', () => {
|
|
|
364
368
|
expect(decodeEventLog).toHaveBeenCalledTimes(2);
|
|
365
369
|
expect(decodeEventLog).toHaveBeenNthCalledWith(1, {
|
|
366
370
|
abi: TOKEN_ROUTER_ABI,
|
|
367
|
-
address:
|
|
371
|
+
address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
|
|
368
372
|
});
|
|
369
373
|
expect(decodeEventLog).toHaveBeenNthCalledWith(2, {
|
|
370
374
|
abi: TOKEN_ROUTER_ABI,
|
|
371
375
|
eventName: 'TransferTokens',
|
|
372
|
-
address:
|
|
376
|
+
address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
|
|
373
377
|
});
|
|
374
378
|
|
|
375
379
|
expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
|
|
@@ -135,7 +135,7 @@ export const trackSourceTx = async (config: BridgeConfig, params: TrackingParams
|
|
|
135
135
|
* Get the `TransferTokens` event's log entry from the receipt
|
|
136
136
|
*/
|
|
137
137
|
const transferEventLog = txReceipt.logs.find((log) => {
|
|
138
|
-
if (log.address === sourceChainData.tokenRouterAddress) {
|
|
138
|
+
if (log.address.toLowerCase() === sourceChainData.tokenRouterAddress.toLowerCase()) {
|
|
139
139
|
const event = decodeEventLog({
|
|
140
140
|
abi: TOKEN_ROUTER_ABI,
|
|
141
141
|
...log,
|
|
@@ -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,
|