@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.
Files changed (76) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +25 -0
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +137 -71
  6. package/dist/index.cjs +11 -3
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +207 -34
  9. package/dist/index.d.ts +207 -34
  10. package/dist/index.js +4 -3
  11. package/dist/index.js.map +1 -1
  12. package/jest.config.js +9 -0
  13. package/package.json +15 -9
  14. package/src/bridges/cctp/__mocks__/asset.mock.ts +15 -0
  15. package/src/bridges/cctp/__mocks__/bridge-transfer.mock.ts +48 -0
  16. package/src/bridges/cctp/__mocks__/chain.mocks.ts +31 -0
  17. package/src/bridges/cctp/__mocks__/config.mock.ts +42 -0
  18. package/src/bridges/cctp/abis/erc20.ts +117 -0
  19. package/src/bridges/cctp/abis/message-transmitter.ts +318 -0
  20. package/src/bridges/cctp/abis/token-router.ts +843 -0
  21. package/src/bridges/cctp/factory.test.ts +73 -0
  22. package/src/bridges/cctp/factory.ts +32 -0
  23. package/src/bridges/cctp/handlers/get-assets.test.ts +47 -0
  24. package/src/bridges/cctp/handlers/get-assets.ts +27 -0
  25. package/src/bridges/cctp/handlers/get-fees.test.ts +61 -0
  26. package/src/bridges/cctp/handlers/get-fees.ts +26 -0
  27. package/src/bridges/cctp/handlers/track-transfer.test.ts +775 -0
  28. package/src/bridges/cctp/handlers/track-transfer.ts +365 -0
  29. package/src/bridges/cctp/handlers/transfer-asset.test.ts +429 -0
  30. package/src/bridges/cctp/handlers/transfer-asset.ts +179 -0
  31. package/src/bridges/cctp/index.ts +1 -0
  32. package/src/bridges/cctp/types/chain.ts +4 -0
  33. package/src/bridges/cctp/types/config.ts +19 -0
  34. package/src/bridges/cctp/utils/config.test.ts +49 -0
  35. package/src/bridges/cctp/utils/config.ts +36 -0
  36. package/src/bridges/cctp/utils/transfer-data.test.ts +83 -0
  37. package/src/bridges/cctp/utils/transfer-data.ts +48 -0
  38. package/src/errors/bridge-error.ts +11 -0
  39. package/src/errors/bridge-initialization-error.ts +9 -0
  40. package/src/errors/bridge-unavailable-error.ts +9 -0
  41. package/src/errors/index.ts +4 -20
  42. package/src/errors/invalid-params-error.ts +9 -0
  43. package/src/index.ts +3 -1
  44. package/src/types/asset.ts +26 -0
  45. package/src/types/bridge.ts +63 -0
  46. package/src/types/chain.ts +10 -0
  47. package/src/types/config.ts +10 -0
  48. package/src/types/environment.ts +4 -0
  49. package/src/types/error.ts +19 -0
  50. package/src/types/index.ts +9 -0
  51. package/src/types/provider.ts +12 -0
  52. package/src/types/signer.ts +18 -0
  53. package/src/types/transfer.ts +35 -0
  54. package/src/unified-bridge-service.test.ts +208 -0
  55. package/src/unified-bridge-service.ts +90 -0
  56. package/src/utils/bridge-types.test.ts +103 -0
  57. package/src/utils/bridge-types.ts +32 -0
  58. package/src/utils/caip2.test.ts +44 -0
  59. package/src/utils/caip2.ts +41 -0
  60. package/src/utils/client.test.ts +97 -0
  61. package/src/utils/client.ts +44 -0
  62. package/src/utils/ensure-config.test.ts +43 -0
  63. package/src/utils/ensure-config.ts +12 -0
  64. package/src/utils/index.ts +2 -0
  65. package/src/utils/network-fee.test.ts +24 -0
  66. package/src/utils/network-fee.ts +6 -0
  67. package/src/utils/retry-promise.test.ts +115 -0
  68. package/src/utils/retry-promise.ts +72 -0
  69. package/src/utils/wait.test.ts +33 -0
  70. package/src/utils/wait.ts +4 -0
  71. package/tsconfig.jest.json +7 -0
  72. package/tsconfig.json +2 -1
  73. package/src/bridge-service.ts +0 -18
  74. package/src/handlers/get-bridge-router.ts +0 -25
  75. package/src/handlers/submit-and-watch-bridge-transaction.ts +0 -1
  76. 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,11 @@
1
+ import type { ErrorCode } from '../types';
2
+
3
+ export class BridgeError extends Error {
4
+ constructor(
5
+ message: string,
6
+ public code: ErrorCode,
7
+ public details?: string,
8
+ ) {
9
+ super(message);
10
+ }
11
+ }
@@ -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
+ }
@@ -1,20 +1,4 @@
1
- export enum ErrorCode {
2
- NOT_IMPLEMENTED = 5001,
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
@@ -1 +1,3 @@
1
- export * from './bridge-service';
1
+ export * from './types';
2
+ export * from './unified-bridge-service';
3
+ export { default as caip2 } from './utils/caip2';
@@ -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 { Address } from 'viem';
2
+ import type { Asset } from './asset';
3
+
4
+ export type Chain = {
5
+ chainName: string;
6
+ chainId: string;
7
+ rpcUrl: string;
8
+ utilityAddresses?: { multicall: Address };
9
+ networkToken: Asset;
10
+ };
@@ -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,4 @@
1
+ export enum Environment {
2
+ PROD = 'production',
3
+ TEST = 'test',
4
+ }
@@ -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,9 @@
1
+ export * from './asset';
2
+ export * from './bridge';
3
+ export * from './chain';
4
+ export * from './config';
5
+ export * from './environment';
6
+ export * from './error';
7
+ export * from './transfer';
8
+ export * from './provider';
9
+ export * from './signer';
@@ -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
+ });