@avalabs/bridge-unified 1.0.1 → 2.0.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 +25 -0
- package/CHANGELOG.md +12 -0
- package/README.md +137 -71
- package/dist/index.cjs +11 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -34
- package/dist/index.d.ts +207 -34
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/jest.config.js +9 -0
- package/package.json +15 -9
- package/src/bridges/cctp/__mocks__/asset.mock.ts +15 -0
- package/src/bridges/cctp/__mocks__/bridge-transfer.mock.ts +48 -0
- package/src/bridges/cctp/__mocks__/chain.mocks.ts +31 -0
- package/src/bridges/cctp/__mocks__/config.mock.ts +42 -0
- package/src/bridges/cctp/abis/erc20.ts +117 -0
- package/src/bridges/cctp/abis/message-transmitter.ts +318 -0
- package/src/bridges/cctp/abis/token-router.ts +843 -0
- package/src/bridges/cctp/factory.test.ts +73 -0
- package/src/bridges/cctp/factory.ts +32 -0
- package/src/bridges/cctp/handlers/get-assets.test.ts +47 -0
- package/src/bridges/cctp/handlers/get-assets.ts +27 -0
- package/src/bridges/cctp/handlers/get-fees.test.ts +61 -0
- package/src/bridges/cctp/handlers/get-fees.ts +26 -0
- package/src/bridges/cctp/handlers/track-transfer.test.ts +775 -0
- package/src/bridges/cctp/handlers/track-transfer.ts +365 -0
- package/src/bridges/cctp/handlers/transfer-asset.test.ts +429 -0
- package/src/bridges/cctp/handlers/transfer-asset.ts +179 -0
- package/src/bridges/cctp/index.ts +1 -0
- package/src/bridges/cctp/types/chain.ts +4 -0
- package/src/bridges/cctp/types/config.ts +19 -0
- package/src/bridges/cctp/utils/config.test.ts +49 -0
- package/src/bridges/cctp/utils/config.ts +36 -0
- package/src/bridges/cctp/utils/transfer-data.test.ts +83 -0
- package/src/bridges/cctp/utils/transfer-data.ts +48 -0
- package/src/errors/bridge-error.ts +11 -0
- package/src/errors/bridge-initialization-error.ts +9 -0
- package/src/errors/bridge-unavailable-error.ts +9 -0
- package/src/errors/index.ts +4 -20
- package/src/errors/invalid-params-error.ts +9 -0
- package/src/index.ts +3 -1
- package/src/types/asset.ts +26 -0
- package/src/types/bridge.ts +63 -0
- package/src/types/chain.ts +10 -0
- package/src/types/config.ts +10 -0
- package/src/types/environment.ts +4 -0
- package/src/types/error.ts +19 -0
- package/src/types/index.ts +9 -0
- package/src/types/provider.ts +12 -0
- package/src/types/signer.ts +18 -0
- package/src/types/transfer.ts +35 -0
- package/src/unified-bridge-service.test.ts +208 -0
- package/src/unified-bridge-service.ts +90 -0
- package/src/utils/bridge-types.test.ts +103 -0
- package/src/utils/bridge-types.ts +32 -0
- package/src/utils/caip2.test.ts +44 -0
- package/src/utils/caip2.ts +41 -0
- package/src/utils/client.test.ts +97 -0
- package/src/utils/client.ts +44 -0
- package/src/utils/ensure-config.test.ts +43 -0
- package/src/utils/ensure-config.ts +12 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/network-fee.test.ts +24 -0
- package/src/utils/network-fee.ts +6 -0
- package/src/utils/retry-promise.test.ts +115 -0
- package/src/utils/retry-promise.ts +72 -0
- package/src/utils/wait.test.ts +33 -0
- package/src/utils/wait.ts +4 -0
- package/tsconfig.jest.json +7 -0
- package/tsconfig.json +2 -1
- package/src/bridge-service.ts +0 -18
- package/src/handlers/get-bridge-router.ts +0 -25
- package/src/handlers/submit-and-watch-bridge-transaction.ts +0 -1
- package/src/handlers/submit-bridge-transaction-step.ts +0 -22
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BridgeInitializationError } from '../../../errors';
|
|
2
|
+
import { ErrorReason } from '../../../types';
|
|
3
|
+
import { Environment } from '../../../types/environment';
|
|
4
|
+
import { AvalancheChainIds } from '../types/chain';
|
|
5
|
+
import type { Config } from '../types/config';
|
|
6
|
+
|
|
7
|
+
const CONFIG_URLS: Record<Environment, string> = {
|
|
8
|
+
[Environment.TEST]:
|
|
9
|
+
'https://raw.githubusercontent.com/ava-labs/avalanche-bridge-resources/main/cctp/cctp_config.test.json',
|
|
10
|
+
[Environment.PROD]:
|
|
11
|
+
'https://raw.githubusercontent.com/ava-labs/avalanche-bridge-resources/main/cctp/cctp_config.json',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getConfig = async (environment: Environment): Promise<Config> => {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(CONFIG_URLS[environment]!);
|
|
17
|
+
const config: Config = await response.json();
|
|
18
|
+
|
|
19
|
+
return config.map((chainData) => ({ ...chainData, chainId: `eip155:${chainData.chainId}` }));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw new BridgeInitializationError(
|
|
22
|
+
ErrorReason.CONFIG_NOT_AVAILABLE,
|
|
23
|
+
`Error while fetching CCTP config: ${(err as unknown as Error).message}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getTrackingDelayByChainId = (chainId: string) => {
|
|
29
|
+
switch (chainId) {
|
|
30
|
+
case AvalancheChainIds.MAINNET:
|
|
31
|
+
case AvalancheChainIds.FUJI:
|
|
32
|
+
return 1000;
|
|
33
|
+
default:
|
|
34
|
+
return 20000;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { InvalidParamsError } from '../../../errors';
|
|
2
|
+
import { ErrorReason, type BridgeAsset, type Chain, type TransferParams } from '../../../types';
|
|
3
|
+
import { BRIDGE_ASSET } from '../__mocks__/asset.mock';
|
|
4
|
+
import { SOURCE_CHAIN, TARGET_CHAIN } from '../__mocks__/chain.mocks';
|
|
5
|
+
import { CCTP_CONFIG } from '../__mocks__/config.mock';
|
|
6
|
+
import type { Config } from '../types/config';
|
|
7
|
+
import { getTransferData } from './transfer-data';
|
|
8
|
+
|
|
9
|
+
describe('CCTP transfer-data', () => {
|
|
10
|
+
const transferData = {
|
|
11
|
+
sourceChain: SOURCE_CHAIN,
|
|
12
|
+
targetChain: TARGET_CHAIN,
|
|
13
|
+
amount: 1000n,
|
|
14
|
+
asset: BRIDGE_ASSET,
|
|
15
|
+
} as unknown as TransferParams;
|
|
16
|
+
|
|
17
|
+
it('throws if source and target chains are the same', () => {
|
|
18
|
+
expect(() =>
|
|
19
|
+
getTransferData(
|
|
20
|
+
{ ...transferData, targetChain: SOURCE_CHAIN as unknown as Chain },
|
|
21
|
+
CCTP_CONFIG as unknown as Config,
|
|
22
|
+
),
|
|
23
|
+
).toThrow(new InvalidParamsError(ErrorReason.IDENTICAL_CHAINS_PROVIDED));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('throws if amount is less than zero', () => {
|
|
27
|
+
expect(() => getTransferData({ ...transferData, amount: -1n }, CCTP_CONFIG as unknown as Config)).toThrow(
|
|
28
|
+
new InvalidParamsError(ErrorReason.INCORRECT_AMOUNT_PROVIDED, 'Amount must be greater than zero'),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('throws if source chain is not supported by CCTP', () => {
|
|
33
|
+
const chainId = 'eip155:999';
|
|
34
|
+
|
|
35
|
+
expect(() =>
|
|
36
|
+
getTransferData(
|
|
37
|
+
{ ...transferData, sourceChain: { ...SOURCE_CHAIN, chainId } as unknown as Chain },
|
|
38
|
+
CCTP_CONFIG as unknown as Config,
|
|
39
|
+
),
|
|
40
|
+
).toThrow(new InvalidParamsError(ErrorReason.CHAIN_NOT_SUPPORTED, `Not supported on source chain "${chainId}"`));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws if target chain is not supported by CCTP', () => {
|
|
44
|
+
const chainId = 'eip155:999';
|
|
45
|
+
|
|
46
|
+
expect(() =>
|
|
47
|
+
getTransferData(
|
|
48
|
+
{ ...transferData, targetChain: { ...TARGET_CHAIN, chainId } as unknown as Chain },
|
|
49
|
+
CCTP_CONFIG as unknown as Config,
|
|
50
|
+
),
|
|
51
|
+
).toThrow(new InvalidParamsError(ErrorReason.CHAIN_NOT_SUPPORTED, `Not supported on target chain "${chainId}"`));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('throws if burn token is not supported', () => {
|
|
55
|
+
const symbol = 'NOT_SUPPORTED';
|
|
56
|
+
|
|
57
|
+
expect(() =>
|
|
58
|
+
getTransferData(
|
|
59
|
+
{ ...transferData, asset: { symbol } as unknown as BridgeAsset },
|
|
60
|
+
CCTP_CONFIG as unknown as Config,
|
|
61
|
+
),
|
|
62
|
+
).toThrow(new InvalidParamsError(ErrorReason.ASSET_NOT_SUPPORTED));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws if mint token is not supported', () => {
|
|
66
|
+
const symbol = 'USDC';
|
|
67
|
+
const config = [{ ...CCTP_CONFIG[0], tokens: [{ symbol: 'NOT_USDC' }] }, CCTP_CONFIG[1]];
|
|
68
|
+
|
|
69
|
+
expect(() =>
|
|
70
|
+
getTransferData({ ...transferData, asset: { symbol } as unknown as BridgeAsset }, config as unknown as Config),
|
|
71
|
+
).toThrow(new InvalidParamsError(ErrorReason.ASSET_NOT_SUPPORTED));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns the correct transfer data', () => {
|
|
75
|
+
const result = getTransferData(transferData, CCTP_CONFIG as unknown as Config);
|
|
76
|
+
expect(result).toStrictEqual({
|
|
77
|
+
sourceChainData: CCTP_CONFIG[0],
|
|
78
|
+
targetChainData: CCTP_CONFIG[1],
|
|
79
|
+
burnToken: CCTP_CONFIG[0].tokens[0],
|
|
80
|
+
mintToken: CCTP_CONFIG[1].tokens[0],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InvalidParamsError } from '../../../errors';
|
|
2
|
+
import { ErrorReason } from '../../../types';
|
|
3
|
+
import type { TransferParams } from '../../../types/bridge';
|
|
4
|
+
import type { Config } from '../types/config';
|
|
5
|
+
|
|
6
|
+
type PartialTransferParams = Pick<TransferParams, 'sourceChain' | 'targetChain' | 'amount' | 'asset'>;
|
|
7
|
+
|
|
8
|
+
export const getTransferData = ({ sourceChain, targetChain, amount, asset }: PartialTransferParams, config: Config) => {
|
|
9
|
+
if (sourceChain.chainId === targetChain.chainId) {
|
|
10
|
+
throw new InvalidParamsError(ErrorReason.IDENTICAL_CHAINS_PROVIDED);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (amount <= 0n) {
|
|
14
|
+
throw new InvalidParamsError(ErrorReason.INCORRECT_AMOUNT_PROVIDED, 'Amount must be greater than zero');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sourceChainData = config.find((chainData) => chainData.chainId === sourceChain.chainId);
|
|
18
|
+
|
|
19
|
+
if (!sourceChainData) {
|
|
20
|
+
throw new InvalidParamsError(
|
|
21
|
+
ErrorReason.CHAIN_NOT_SUPPORTED,
|
|
22
|
+
`Not supported on source chain "${sourceChain.chainId}"`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const targetChainData = config.find((chainData) => chainData.chainId === targetChain.chainId);
|
|
27
|
+
|
|
28
|
+
if (!targetChainData) {
|
|
29
|
+
throw new InvalidParamsError(
|
|
30
|
+
ErrorReason.CHAIN_NOT_SUPPORTED,
|
|
31
|
+
`Not supported on target chain "${targetChain.chainId}"`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const burnToken = sourceChainData.tokens.find((token) => token.symbol === asset.symbol);
|
|
36
|
+
const mintToken = targetChainData.tokens.find((token) => token.symbol === asset.symbol);
|
|
37
|
+
|
|
38
|
+
if (!burnToken || !mintToken) {
|
|
39
|
+
throw new InvalidParamsError(ErrorReason.ASSET_NOT_SUPPORTED);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
sourceChainData,
|
|
44
|
+
targetChainData,
|
|
45
|
+
burnToken,
|
|
46
|
+
mintToken,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ErrorCode, ErrorReason } from '../types';
|
|
2
|
+
import { BridgeError } from './bridge-error';
|
|
3
|
+
|
|
4
|
+
export class BridgeInitializationError extends BridgeError {
|
|
5
|
+
constructor(message = ErrorReason.UNKNOWN, details?: string) {
|
|
6
|
+
super(message, ErrorCode.INITIALIZATION_FAILED, details);
|
|
7
|
+
this.name = 'BridgeInitializationError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ErrorCode, ErrorReason } from '../types';
|
|
2
|
+
import { BridgeError } from './bridge-error';
|
|
3
|
+
|
|
4
|
+
export class BridgeUnavailableError extends BridgeError {
|
|
5
|
+
constructor(message = ErrorReason.UNKNOWN, details?: string) {
|
|
6
|
+
super(message, ErrorCode.BRIDGE_NOT_AVAILABLE, details);
|
|
7
|
+
this.name = 'BridgeUnavailableError';
|
|
8
|
+
}
|
|
9
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class BridgeError extends Error {
|
|
6
|
-
constructor(
|
|
7
|
-
message: string,
|
|
8
|
-
public code: ErrorCode,
|
|
9
|
-
) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.code = code;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class NotImplementedError extends BridgeError {
|
|
16
|
-
constructor(message = 'Not implemented') {
|
|
17
|
-
super(message, ErrorCode.NOT_IMPLEMENTED);
|
|
18
|
-
this.name = 'NotImplementedError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
export * from './bridge-error';
|
|
2
|
+
export * from './bridge-unavailable-error';
|
|
3
|
+
export * from './bridge-initialization-error';
|
|
4
|
+
export * from './invalid-params-error';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ErrorCode, ErrorReason } from '../types';
|
|
2
|
+
import { BridgeError } from './bridge-error';
|
|
3
|
+
|
|
4
|
+
export class InvalidParamsError extends BridgeError {
|
|
5
|
+
constructor(message = ErrorReason.INVALID_PARAMS, details?: string) {
|
|
6
|
+
super(message, ErrorCode.INVALID_PARAMS, details);
|
|
7
|
+
this.name = 'InvalidParamsError';
|
|
8
|
+
}
|
|
9
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Address } from 'viem';
|
|
2
|
+
import type { BridgeType } from './bridge';
|
|
3
|
+
|
|
4
|
+
export enum TokenType {
|
|
5
|
+
NATIVE = 'native',
|
|
6
|
+
ERC20 = 'erc20',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Asset = {
|
|
10
|
+
type: TokenType;
|
|
11
|
+
address?: Address;
|
|
12
|
+
name: string;
|
|
13
|
+
symbol: string;
|
|
14
|
+
decimals: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// chainId - bridge type pairs
|
|
18
|
+
export type DestinationInfo = Record<string, BridgeType[]>;
|
|
19
|
+
|
|
20
|
+
export type BridgeAsset = Asset & {
|
|
21
|
+
destinations: DestinationInfo;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ChainAssetMap = Record<string, BridgeAsset[]>;
|
|
25
|
+
|
|
26
|
+
export type AssetFeeMap = Record<Address, bigint>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AssetFeeMap, BridgeAsset, ChainAssetMap } from './asset';
|
|
2
|
+
import type { Chain } from './chain';
|
|
3
|
+
import type { BridgeConfig } from './config';
|
|
4
|
+
import type { Environment } from './environment';
|
|
5
|
+
import type { Provider } from './provider';
|
|
6
|
+
import type { Signer } from './signer';
|
|
7
|
+
import type { BridgeTransfer } from './transfer';
|
|
8
|
+
|
|
9
|
+
export enum BridgeType {
|
|
10
|
+
CCTP = 'cctp',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FeeParams = {
|
|
14
|
+
asset: BridgeAsset;
|
|
15
|
+
amount: bigint;
|
|
16
|
+
sourceChain: Chain;
|
|
17
|
+
targetChain: Chain;
|
|
18
|
+
provider?: Provider;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export enum BridgeSignatureReason {
|
|
22
|
+
AllowanceApproval = 'allowance-approval',
|
|
23
|
+
TokensTransfer = 'tokens-transfer',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type BridgeStepDetails = {
|
|
27
|
+
currentSignature: number;
|
|
28
|
+
requiredSignatures: number;
|
|
29
|
+
currentSignatureReason: BridgeSignatureReason;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type TransferParams = {
|
|
33
|
+
asset: BridgeAsset;
|
|
34
|
+
amount: bigint;
|
|
35
|
+
fromAddress: string;
|
|
36
|
+
toAddress?: string;
|
|
37
|
+
sourceChain: Chain;
|
|
38
|
+
targetChain: Chain;
|
|
39
|
+
sourceProvider?: Provider;
|
|
40
|
+
targetProvider?: Provider;
|
|
41
|
+
onStepChange?: (stepDetails: BridgeStepDetails) => void;
|
|
42
|
+
sign?: Signer;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type TrackingParams = {
|
|
46
|
+
bridgeTransfer: BridgeTransfer;
|
|
47
|
+
sourceProvider?: Provider;
|
|
48
|
+
targetProvider?: Provider;
|
|
49
|
+
updateListener: (transfer: BridgeTransfer) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type BridgeService = {
|
|
53
|
+
type: BridgeType;
|
|
54
|
+
config: BridgeConfig | null;
|
|
55
|
+
ensureHasConfig: () => Promise<void>;
|
|
56
|
+
updateConfig: () => Promise<void>;
|
|
57
|
+
getAssets: () => Promise<ChainAssetMap>;
|
|
58
|
+
getFees: (params: FeeParams) => Promise<AssetFeeMap>;
|
|
59
|
+
transferAsset: (params: TransferParams) => Promise<BridgeTransfer>;
|
|
60
|
+
trackTransfer: (transfer: TrackingParams) => { cancel: () => void; result: Promise<BridgeTransfer> };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type BridgeServiceFactory = (environment: Environment) => BridgeService;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config as CctpConfig } from '../bridges/cctp/types/config';
|
|
2
|
+
import type { BridgeType } from './bridge';
|
|
3
|
+
import type { Environment } from './environment';
|
|
4
|
+
|
|
5
|
+
export type BridgeServiceConfig = {
|
|
6
|
+
environment: Environment;
|
|
7
|
+
disabledBridgeTypes?: BridgeType[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BridgeConfig = CctpConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export enum ErrorCode {
|
|
2
|
+
BRIDGE_NOT_AVAILABLE = 5001,
|
|
3
|
+
INITIALIZATION_FAILED = 5002,
|
|
4
|
+
INVALID_PARAMS = 5003,
|
|
5
|
+
TIMEOUT = 5004,
|
|
6
|
+
TRANSACTION_REVERTED = 5005,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export enum ErrorReason {
|
|
10
|
+
UNKNOWN = 'UNKNOWN', // generic, not specified error
|
|
11
|
+
CONFIG_NOT_AVAILABLE = 'CONFIG_NOT_AVAILABLE', // error while fetching or parsing the config
|
|
12
|
+
INVALID_PARAMS = 'INVALID_PARAMS', // generic error with the params
|
|
13
|
+
IDENTICAL_CHAINS_PROVIDED = 'IDENTICAL_CHAINS_PROVIDED', // provided source and target chains are the same
|
|
14
|
+
INCORRECT_AMOUNT_PROVIDED = 'INCORRECT_AMOUNT_PROVIDED', // the transfer amount is incorrect (e.g.: lesser than or equal to zero)
|
|
15
|
+
INCORRECT_ADDRESS_PROVIDED = 'INCORRECT_ADDRESS_PROVIDED', // the sender or recipient address is incorrect
|
|
16
|
+
CHAIN_NOT_SUPPORTED = 'CHAIN_NOT_SUPPORTED', // the provided source or target chain is not supported by the bridge
|
|
17
|
+
ASSET_NOT_SUPPORTED = 'ASSET_NOT_SUPPORTED', // the provided asset is not supported by the bridge
|
|
18
|
+
CONFIRMATION_COUNT_UNKNOWN = 'CONFIRMATION_COUNT_UNKNOWN', // required confirmation count of the source or target chain is unknown
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type RequestArguments = {
|
|
2
|
+
method: string;
|
|
3
|
+
params?: unknown[] | Record<string | number, unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Provider = {
|
|
7
|
+
/**
|
|
8
|
+
* EIP-1193 compatible request method
|
|
9
|
+
* https://eips.ethereum.org/EIPS/eip-1193#request-1
|
|
10
|
+
*/
|
|
11
|
+
request: (args: RequestArguments) => Promise<unknown>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Hex = `0x${string}`;
|
|
2
|
+
|
|
3
|
+
export type TransactionRequest = {
|
|
4
|
+
type?: number | null;
|
|
5
|
+
data?: Hex | null;
|
|
6
|
+
from: Hex;
|
|
7
|
+
gas?: bigint;
|
|
8
|
+
nonce?: number;
|
|
9
|
+
to?: Hex | null;
|
|
10
|
+
value?: bigint;
|
|
11
|
+
gasPrice?: bigint | null;
|
|
12
|
+
gasLimit?: bigint | null;
|
|
13
|
+
maxPriorityFeePerGas?: bigint | null;
|
|
14
|
+
maxFeePerGas?: bigint | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Dispatch = (signedTxHash: Hex) => Promise<Hex>;
|
|
18
|
+
export type Signer = (data: TransactionRequest, dispatch: Dispatch) => Promise<Hex>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Environment } from './environment';
|
|
2
|
+
import type { BridgeType } from './bridge';
|
|
3
|
+
import type { Chain } from './chain';
|
|
4
|
+
import type { ErrorCode } from './error';
|
|
5
|
+
|
|
6
|
+
export type BridgeTransfer = {
|
|
7
|
+
type: BridgeType;
|
|
8
|
+
environment: Environment;
|
|
9
|
+
fromAddress: string;
|
|
10
|
+
toAddress: string;
|
|
11
|
+
amount: bigint;
|
|
12
|
+
amountDecimals: number;
|
|
13
|
+
symbol: string;
|
|
14
|
+
|
|
15
|
+
completedAt?: number;
|
|
16
|
+
errorCode?: ErrorCode;
|
|
17
|
+
bridgeFee: bigint;
|
|
18
|
+
|
|
19
|
+
sourceChain: Chain;
|
|
20
|
+
sourceStartedAt: number;
|
|
21
|
+
sourceTxHash: string;
|
|
22
|
+
sourceNetworkFee?: bigint;
|
|
23
|
+
sourceConfirmationCount: number;
|
|
24
|
+
requiredSourceConfirmationCount: number;
|
|
25
|
+
|
|
26
|
+
targetChain: Chain;
|
|
27
|
+
targetStartedAt?: number;
|
|
28
|
+
targetTxHash?: string;
|
|
29
|
+
targetNetworkFee?: bigint;
|
|
30
|
+
targetConfirmationCount: number;
|
|
31
|
+
requiredTargetConfirmationCount: number;
|
|
32
|
+
|
|
33
|
+
startBlockNumber?: bigint;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { BridgeUnavailableError } from './errors';
|
|
2
|
+
import {
|
|
3
|
+
Environment,
|
|
4
|
+
type BridgeService,
|
|
5
|
+
type BridgeType,
|
|
6
|
+
type FeeParams,
|
|
7
|
+
type TransferParams,
|
|
8
|
+
type TrackingParams,
|
|
9
|
+
type BridgeAsset,
|
|
10
|
+
} from './types';
|
|
11
|
+
import { createUnifiedBridgeService } from './unified-bridge-service';
|
|
12
|
+
import { getBridgeForTransfer, getEnabledBridgeServices } from './utils';
|
|
13
|
+
|
|
14
|
+
jest.mock('./utils');
|
|
15
|
+
|
|
16
|
+
const getBridgeMock = () => ({
|
|
17
|
+
updateConfig: jest.fn(),
|
|
18
|
+
getAssets: jest.fn(),
|
|
19
|
+
getFees: jest.fn(),
|
|
20
|
+
transferAsset: jest.fn(),
|
|
21
|
+
trackTransfer: jest.fn(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('unified-bridge-service', () => {
|
|
25
|
+
const environment = Environment.TEST;
|
|
26
|
+
const [bridge1, bridge2, bridge3] = [getBridgeMock(), getBridgeMock(), getBridgeMock()];
|
|
27
|
+
const enabledBridges = new Map([
|
|
28
|
+
['bridge1', bridge1],
|
|
29
|
+
['bridge2', bridge2],
|
|
30
|
+
['bridge3', bridge3],
|
|
31
|
+
]) as unknown as Map<BridgeType, BridgeService>;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.resetAllMocks();
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
jest.mocked(getEnabledBridgeServices).mockReturnValue(enabledBridges);
|
|
37
|
+
jest
|
|
38
|
+
.mocked(getBridgeForTransfer)
|
|
39
|
+
.mockReturnValue({ type: 'bridge2' as unknown as BridgeType, bridge: bridge2 as unknown as BridgeService });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('creates a new unified service correctly', () => {
|
|
43
|
+
const service = createUnifiedBridgeService({ environment });
|
|
44
|
+
|
|
45
|
+
expect(getEnabledBridgeServices).toHaveBeenCalledWith(environment, undefined);
|
|
46
|
+
expect(service).toStrictEqual({
|
|
47
|
+
environment,
|
|
48
|
+
bridges: enabledBridges,
|
|
49
|
+
init: expect.any(Function),
|
|
50
|
+
updateConfigs: expect.any(Function),
|
|
51
|
+
getAssets: expect.any(Function),
|
|
52
|
+
getFees: expect.any(Function),
|
|
53
|
+
transferAsset: expect.any(Function),
|
|
54
|
+
trackTransfer: expect.any(Function),
|
|
55
|
+
canTransferAsset: expect.any(Function),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('initializes the service correctly', async () => {
|
|
60
|
+
const service = createUnifiedBridgeService({ environment });
|
|
61
|
+
|
|
62
|
+
await expect(service.init()).resolves.toBeUndefined();
|
|
63
|
+
expect(bridge1.updateConfig).toHaveBeenCalled();
|
|
64
|
+
expect(bridge2.updateConfig).toHaveBeenCalled();
|
|
65
|
+
expect(bridge3.updateConfig).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('attempts to update bridge configs correctly', async () => {
|
|
69
|
+
bridge2.updateConfig.mockRejectedValueOnce(new Error('some error'));
|
|
70
|
+
const service = createUnifiedBridgeService({ environment });
|
|
71
|
+
|
|
72
|
+
await expect(service.updateConfigs()).resolves.toBeUndefined();
|
|
73
|
+
expect(bridge1.updateConfig).toHaveBeenCalled();
|
|
74
|
+
expect(bridge2.updateConfig).toHaveBeenCalled();
|
|
75
|
+
expect(bridge3.updateConfig).toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns the aggregated asset list correctly', async () => {
|
|
79
|
+
bridge1.getAssets.mockResolvedValueOnce({
|
|
80
|
+
'eip155:1': [
|
|
81
|
+
{ symbol: 'TOK1', destinations: { 'eip155:2': ['bridge1'] } },
|
|
82
|
+
{ symbol: 'TOK2', destinations: { 'eip155:2': ['bridge1'] } },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
bridge2.getAssets.mockResolvedValueOnce({
|
|
86
|
+
'eip155:1': [
|
|
87
|
+
{ symbol: 'TOK1', destinations: { 'eip155:2': ['bridge2'] } },
|
|
88
|
+
{ symbol: 'TOK3', destinations: { 'eip155:3': ['bridge2'] } },
|
|
89
|
+
],
|
|
90
|
+
'eip155:2': [
|
|
91
|
+
{ symbol: 'TOK4', destinations: { 'eip155:1': ['bridge2'] } },
|
|
92
|
+
{ symbol: 'TOK5', destinations: { 'eip155:3': ['bridge2'] } },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
bridge3.getAssets.mockResolvedValueOnce({
|
|
96
|
+
'eip155:1': [{ symbol: 'TOK1', destinations: { 'eip155:3': ['bridge3'] } }],
|
|
97
|
+
'eip155:3': [{ symbol: 'TOK6', destinations: { 'eip155:1': ['bridge3'] } }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const service = createUnifiedBridgeService({ environment });
|
|
101
|
+
const assets = await service.getAssets();
|
|
102
|
+
|
|
103
|
+
expect(assets).toStrictEqual({
|
|
104
|
+
'eip155:1': [
|
|
105
|
+
{ symbol: 'TOK1', destinations: { 'eip155:2': ['bridge1', 'bridge2'], 'eip155:3': ['bridge3'] } },
|
|
106
|
+
{ symbol: 'TOK2', destinations: { 'eip155:2': ['bridge1'] } },
|
|
107
|
+
{ symbol: 'TOK3', destinations: { 'eip155:3': ['bridge2'] } },
|
|
108
|
+
],
|
|
109
|
+
'eip155:2': [
|
|
110
|
+
{ symbol: 'TOK4', destinations: { 'eip155:1': ['bridge2'] } },
|
|
111
|
+
{ symbol: 'TOK5', destinations: { 'eip155:3': ['bridge2'] } },
|
|
112
|
+
],
|
|
113
|
+
'eip155:3': [{ symbol: 'TOK6', destinations: { 'eip155:1': ['bridge3'] } }],
|
|
114
|
+
});
|
|
115
|
+
expect(bridge1.getAssets).toHaveBeenCalled();
|
|
116
|
+
expect(bridge2.getAssets).toHaveBeenCalled();
|
|
117
|
+
expect(bridge3.getAssets).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns the fee of the correct bridge', async () => {
|
|
121
|
+
const fee = 2000n;
|
|
122
|
+
const feeParams = {
|
|
123
|
+
asset: { symbol: 'TOK1' },
|
|
124
|
+
targetChain: { chainId: 'eip155:1' },
|
|
125
|
+
} as unknown as FeeParams;
|
|
126
|
+
|
|
127
|
+
bridge2.getFees.mockResolvedValueOnce(fee);
|
|
128
|
+
|
|
129
|
+
const service = createUnifiedBridgeService({ environment });
|
|
130
|
+
const result = await service.getFees(feeParams);
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(fee);
|
|
133
|
+
expect(getBridgeForTransfer).toHaveBeenCalledWith(enabledBridges, feeParams.asset, feeParams.targetChain.chainId);
|
|
134
|
+
expect(bridge1.getFees).not.toHaveBeenCalled();
|
|
135
|
+
expect(bridge2.getFees).toHaveBeenCalledWith(feeParams);
|
|
136
|
+
expect(bridge3.getFees).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('starts a transfer using the correct bridge', async () => {
|
|
140
|
+
const transferParams = {
|
|
141
|
+
asset: { symbol: 'TOK1' },
|
|
142
|
+
targetChain: { chainId: 'eip155:1' },
|
|
143
|
+
} as unknown as TransferParams;
|
|
144
|
+
const bridgeTransfer = { type: 'bridge2' };
|
|
145
|
+
|
|
146
|
+
bridge2.transferAsset.mockResolvedValueOnce(bridgeTransfer);
|
|
147
|
+
|
|
148
|
+
const service = createUnifiedBridgeService({ environment });
|
|
149
|
+
const result = await service.transferAsset(transferParams);
|
|
150
|
+
|
|
151
|
+
expect(result).toStrictEqual(bridgeTransfer);
|
|
152
|
+
expect(getBridgeForTransfer).toHaveBeenCalledWith(
|
|
153
|
+
enabledBridges,
|
|
154
|
+
transferParams.asset,
|
|
155
|
+
transferParams.targetChain.chainId,
|
|
156
|
+
);
|
|
157
|
+
expect(bridge1.transferAsset).not.toHaveBeenCalled();
|
|
158
|
+
expect(bridge2.transferAsset).toHaveBeenCalledWith(transferParams);
|
|
159
|
+
expect(bridge3.transferAsset).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('throws if there is no enabled bridge for transfer tracking', () => {
|
|
163
|
+
const trackingParams = {
|
|
164
|
+
bridgeTransfer: {
|
|
165
|
+
type: 'not-enabled-bridge',
|
|
166
|
+
},
|
|
167
|
+
} as unknown as TrackingParams;
|
|
168
|
+
|
|
169
|
+
const service = createUnifiedBridgeService({ environment });
|
|
170
|
+
expect(() => service.trackTransfer(trackingParams)).toThrow(BridgeUnavailableError);
|
|
171
|
+
expect(bridge1.trackTransfer).not.toHaveBeenCalled();
|
|
172
|
+
expect(bridge2.trackTransfer).not.toHaveBeenCalled();
|
|
173
|
+
expect(bridge3.trackTransfer).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('tracks a transfer using the correct bridge', () => {
|
|
177
|
+
const trackingParams = {
|
|
178
|
+
bridgeTransfer: {
|
|
179
|
+
type: 'bridge2',
|
|
180
|
+
},
|
|
181
|
+
} as unknown as TrackingParams;
|
|
182
|
+
const trackingResult = {
|
|
183
|
+
cancel: jest.fn(),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
bridge2.trackTransfer.mockReturnValueOnce(trackingResult);
|
|
187
|
+
|
|
188
|
+
const service = createUnifiedBridgeService({ environment });
|
|
189
|
+
const result = service.trackTransfer(trackingParams);
|
|
190
|
+
|
|
191
|
+
expect(result).toStrictEqual(trackingResult);
|
|
192
|
+
expect(bridge1.trackTransfer).not.toHaveBeenCalled();
|
|
193
|
+
expect(bridge2.trackTransfer).toHaveBeenCalledWith(trackingParams);
|
|
194
|
+
expect(bridge3.trackTransfer).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('can tell whether it can transfer an asset to target chain ahead of time', () => {
|
|
198
|
+
const service = createUnifiedBridgeService({ environment });
|
|
199
|
+
|
|
200
|
+
expect(service.canTransferAsset({ symbol: 'TOK1' } as unknown as BridgeAsset, 'eip155:1')).toBe(true);
|
|
201
|
+
|
|
202
|
+
jest.mocked(getBridgeForTransfer).mockImplementationOnce(() => {
|
|
203
|
+
throw new BridgeUnavailableError();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(service.canTransferAsset({ symbol: 'TOK1' } as unknown as BridgeAsset, 'eip155:2')).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|