@avalabs/bridge-unified 1.0.1 → 2.0.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.
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 +18 -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 +45 -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 +779 -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,90 @@
1
+ import type { FeeParams, TrackingParams, TransferParams } from './types/bridge';
2
+ import type { BridgeServiceConfig } from './types/config';
3
+ import { getBridgeForTransfer, getEnabledBridgeServices } from './utils';
4
+ import type { BridgeAsset, ChainAssetMap } from './types';
5
+ import { isArray, mergeWith } from 'lodash';
6
+ import { BridgeUnavailableError } from './errors';
7
+
8
+ export const createUnifiedBridgeService = ({ environment, disabledBridgeTypes }: BridgeServiceConfig) => {
9
+ const enabledBridgeServices = getEnabledBridgeServices(environment, disabledBridgeTypes);
10
+
11
+ const updateConfigs = async () => {
12
+ await Promise.allSettled(
13
+ Array.from(enabledBridgeServices).map(([, bridgeService]) => bridgeService.updateConfig()),
14
+ );
15
+ };
16
+
17
+ const init = async () => {
18
+ await updateConfigs();
19
+ };
20
+
21
+ const getAssets = async () => {
22
+ const assets = await Promise.all(Array.from(enabledBridgeServices).map(([, bridge]) => bridge.getAssets()));
23
+ return assets.reduce<ChainAssetMap>((aggregatedAssets, chainAssetMap) => {
24
+ for (const [chainId, bridgeAssets] of Object.entries(chainAssetMap)) {
25
+ const existingAssets = aggregatedAssets[chainId];
26
+
27
+ if (existingAssets) {
28
+ for (const bridgeAsset of bridgeAssets) {
29
+ const index = existingAssets.findIndex(({ symbol }) => symbol === bridgeAsset.symbol);
30
+
31
+ if (index === -1) {
32
+ existingAssets.push(bridgeAsset);
33
+ } else {
34
+ mergeWith(existingAssets[index], bridgeAsset, (objValue, srcValue) => {
35
+ if (isArray(objValue)) {
36
+ return [...new Set(objValue.concat(srcValue))];
37
+ }
38
+ });
39
+ }
40
+ }
41
+ } else {
42
+ aggregatedAssets[chainId] = bridgeAssets;
43
+ }
44
+ }
45
+
46
+ return aggregatedAssets;
47
+ }, {});
48
+ };
49
+
50
+ const getFees = async (params: FeeParams) => {
51
+ const { bridge } = getBridgeForTransfer(enabledBridgeServices, params.asset, params.targetChain.chainId);
52
+ return bridge.getFees(params);
53
+ };
54
+
55
+ const transferAsset = async (params: TransferParams) => {
56
+ const { bridge } = getBridgeForTransfer(enabledBridgeServices, params.asset, params.targetChain.chainId);
57
+ return bridge.transferAsset(params);
58
+ };
59
+
60
+ const canTransferAsset = (asset: BridgeAsset, targetChainId: string) => {
61
+ try {
62
+ getBridgeForTransfer(enabledBridgeServices, asset, targetChainId);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ };
68
+
69
+ const trackTransfer = (params: TrackingParams) => {
70
+ const bridge = enabledBridgeServices.get(params.bridgeTransfer.type);
71
+
72
+ if (!bridge) {
73
+ throw new BridgeUnavailableError();
74
+ }
75
+
76
+ return bridge.trackTransfer(params);
77
+ };
78
+
79
+ return {
80
+ environment,
81
+ bridges: enabledBridgeServices,
82
+ init,
83
+ updateConfigs,
84
+ getAssets,
85
+ getFees,
86
+ canTransferAsset,
87
+ transferAsset,
88
+ trackTransfer,
89
+ };
90
+ };
@@ -0,0 +1,103 @@
1
+ import { BridgeUnavailableError } from '../errors';
2
+ import {
3
+ Environment,
4
+ type BridgeType,
5
+ type BridgeServiceFactory,
6
+ type BridgeService,
7
+ type BridgeAsset,
8
+ } from '../types';
9
+ import * as bridgeTypes from './bridge-types';
10
+
11
+ const getBridgeMock = () => ({
12
+ updateConfig: jest.fn(),
13
+ getAssets: jest.fn(),
14
+ getFees: jest.fn(),
15
+ transferAsset: jest.fn(),
16
+ trackTransfer: jest.fn(),
17
+ });
18
+
19
+ describe('bridge-types', () => {
20
+ const environment = Environment.TEST;
21
+
22
+ beforeEach(() => {
23
+ jest.resetAllMocks();
24
+ });
25
+
26
+ describe('getEnabledBridgeServices', () => {
27
+ let realSupportedBridges: Map<BridgeType, BridgeServiceFactory>;
28
+
29
+ const [bridge1, bridge2, bridge3] = [jest.fn(), jest.fn(), jest.fn()];
30
+ const enabledBridges = new Map([
31
+ ['bridge1', bridge1],
32
+ ['bridge2', bridge2],
33
+ ['bridge3', bridge3],
34
+ ]) as unknown as Map<BridgeType, BridgeServiceFactory>;
35
+
36
+ beforeAll(() => {
37
+ realSupportedBridges = bridgeTypes.supportedBridges;
38
+
39
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
40
+ // @ts-ignore
41
+ bridgeTypes.supportedBridges = enabledBridges;
42
+ });
43
+
44
+ afterAll(() => {
45
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
46
+ // @ts-ignore
47
+ bridgeTypes.supportedBridges = realSupportedBridges;
48
+ });
49
+
50
+ beforeEach(() => {
51
+ bridge1.mockReturnValueOnce({ type: 'bridge1' });
52
+ bridge2.mockReturnValueOnce({ type: 'bridge2' });
53
+ bridge3.mockReturnValueOnce({ type: 'bridge3' });
54
+ });
55
+
56
+ it('returns all of the supported bridges correctly', () => {
57
+ const bridges = bridgeTypes.getEnabledBridgeServices(environment);
58
+ expect(bridges).toStrictEqual(
59
+ new Map([
60
+ ['bridge1', { type: 'bridge1' }],
61
+ ['bridge2', { type: 'bridge2' }],
62
+ ['bridge3', { type: 'bridge3' }],
63
+ ]),
64
+ );
65
+ });
66
+
67
+ it('returns all of the enabled bridges correctly', () => {
68
+ const bridges = bridgeTypes.getEnabledBridgeServices(environment, ['bridge2' as unknown as BridgeType]);
69
+ expect(bridges).toStrictEqual(
70
+ new Map([
71
+ ['bridge1', { type: 'bridge1' }],
72
+ ['bridge3', { type: 'bridge3' }],
73
+ ]),
74
+ );
75
+ });
76
+ });
77
+
78
+ describe('getBridgeForTransfer', () => {
79
+ const [bridge1, bridge2, bridge3] = [getBridgeMock(), getBridgeMock(), getBridgeMock()];
80
+ const enabledBridges = new Map([
81
+ ['bridge1', bridge1],
82
+ ['bridge2', bridge2],
83
+ ['bridge3', bridge3],
84
+ ]) as unknown as Map<BridgeType, BridgeService>;
85
+ const asset = {
86
+ destinations: {
87
+ 'eip155:2': ['bridge1', 'bridge3'],
88
+ },
89
+ } as unknown as BridgeAsset;
90
+
91
+ it('throws if no enabled bridge found', () => {
92
+ expect(() => bridgeTypes.getBridgeForTransfer(enabledBridges, asset, 'eip155:1')).toThrow(BridgeUnavailableError);
93
+ });
94
+
95
+ it('returns the correct bridge for the transfer', () => {
96
+ const result = bridgeTypes.getBridgeForTransfer(enabledBridges, asset, 'eip155:2');
97
+ expect(result).toStrictEqual({
98
+ type: 'bridge1',
99
+ bridge: bridge1,
100
+ });
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,32 @@
1
+ import { cctpBridgeFactory } from '../bridges/cctp/factory';
2
+ import { BridgeUnavailableError } from '../errors';
3
+ import type { BridgeAsset } from '../types/asset';
4
+ import { type BridgeService, BridgeType } from '../types/bridge';
5
+ import { Environment } from '../types/environment';
6
+
7
+ export const supportedBridges = new Map([[BridgeType.CCTP, cctpBridgeFactory]]);
8
+
9
+ export const getEnabledBridgeServices = (environment: Environment, disabledBridgeTypes?: BridgeType[]) => {
10
+ return new Map(
11
+ [...supportedBridges]
12
+ .filter(([bridgeType]) => !disabledBridgeTypes?.includes(bridgeType))
13
+ .map(([bridgeType, factory]) => [bridgeType, factory(environment)]),
14
+ );
15
+ };
16
+
17
+ export const getBridgeForTransfer = (
18
+ enabledBridgeServices: Map<BridgeType, BridgeService>,
19
+ asset: BridgeAsset,
20
+ targetChainId: string,
21
+ ) => {
22
+ const bridgeType = asset.destinations[targetChainId]?.find((bridgeType) => enabledBridgeServices.has(bridgeType));
23
+
24
+ if (!bridgeType) {
25
+ throw new BridgeUnavailableError();
26
+ }
27
+
28
+ return {
29
+ type: bridgeType,
30
+ bridge: enabledBridgeServices.get(bridgeType)!,
31
+ };
32
+ };
@@ -0,0 +1,44 @@
1
+ import caip2 from './caip2';
2
+
3
+ describe('caip2', () => {
4
+ describe('toJSON', () => {
5
+ it.each(['', '1.', '1:', ':2'])('throws when the identifier is invalid', (identifier) => {
6
+ expect(() => caip2.toJSON(identifier)).toThrow('Invalid identifier provided.');
7
+ });
8
+
9
+ it.each(['1:2', 'toolongnamespace:2', '!:2'])('throws when the namespace is invalid', (identifier) => {
10
+ expect(() => caip2.toJSON(identifier)).toThrow('Invalid namespace provided.');
11
+ });
12
+
13
+ it.each(['eip155:!', 'eip155:toolong-2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM'])(
14
+ 'throws when the reference is invalid',
15
+ (identifier) => {
16
+ expect(() => caip2.toJSON(identifier)).toThrow('Invalid reference provided.');
17
+ },
18
+ );
19
+
20
+ it.each([
21
+ {
22
+ identifier: 'eip155:1',
23
+ expected: { namespace: 'eip155', reference: '1' },
24
+ },
25
+ {
26
+ identifier: 'bip122:000000000019d6689c085ae165831e93',
27
+ expected: { namespace: 'bip122', reference: '000000000019d6689c085ae165831e93' },
28
+ },
29
+ {
30
+ identifier: 'ava:2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM',
31
+ expected: { namespace: 'ava', reference: '2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM' },
32
+ },
33
+ ])('returns the correct caip2 object', ({ identifier, expected }) => {
34
+ expect(caip2.toJSON(identifier)).toStrictEqual(expected);
35
+ });
36
+ });
37
+
38
+ it('returns the correct identifier', () => {
39
+ const namespace = 'eip155';
40
+ const reference = '1';
41
+
42
+ expect(caip2.toString({ namespace, reference })).toBe('eip155:1');
43
+ });
44
+ });
@@ -0,0 +1,41 @@
1
+ // ref: https://chainagnostic.org/CAIPs/caip-2
2
+
3
+ export type Caip2ChainId = {
4
+ namespace: string;
5
+ reference: string;
6
+ };
7
+
8
+ const namespacePattern = '^[-a-z0-9]{3,8}$';
9
+ // the standard allows up to 32 characters for the reference part, but we have to set it to 50 so it accepts our cb58 encoded chain IDs
10
+ const referencePattern = '^[-_a-zA-Z0-9]{1,50}$';
11
+ const delimeter = ':';
12
+
13
+ const toJSON = (identifier: string): Caip2ChainId => {
14
+ const [namespace, reference] = identifier.split(delimeter);
15
+
16
+ if (!namespace || !reference) {
17
+ throw new Error('Invalid identifier provided.');
18
+ }
19
+
20
+ if (!new RegExp(namespacePattern).test(namespace)) {
21
+ throw new Error('Invalid namespace provided.');
22
+ }
23
+
24
+ if (!new RegExp(referencePattern).test(reference)) {
25
+ throw new Error('Invalid reference provided.');
26
+ }
27
+
28
+ return {
29
+ namespace,
30
+ reference,
31
+ };
32
+ };
33
+
34
+ const toString = ({ namespace, reference }: Caip2ChainId) => {
35
+ return `${namespace}${delimeter}${reference}`;
36
+ };
37
+
38
+ export default {
39
+ toJSON,
40
+ toString,
41
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ custom,
3
+ http,
4
+ type CustomTransport,
5
+ type HttpTransport,
6
+ createWalletClient,
7
+ type WalletClient,
8
+ publicActions,
9
+ } from 'viem';
10
+ import { TokenType, type Chain, type Provider } from '../types';
11
+ import { getClientForChain } from './client';
12
+
13
+ jest.mock('viem');
14
+
15
+ const chainMock: Chain = {
16
+ chainId: 'eip155:1',
17
+ chainName: 'test chain',
18
+ networkToken: {
19
+ decimals: 18,
20
+ symbol: 'TEST',
21
+ name: 'Test Token',
22
+ type: TokenType.NATIVE,
23
+ },
24
+ rpcUrl: 'http://test.com',
25
+ utilityAddresses: {
26
+ multicall: '0x1',
27
+ },
28
+ };
29
+
30
+ const chainInfo = {
31
+ id: 1,
32
+ name: chainMock.chainName,
33
+ nativeCurrency: {
34
+ decimals: chainMock.networkToken.decimals,
35
+ symbol: chainMock.networkToken.symbol,
36
+ name: chainMock.networkToken.name,
37
+ },
38
+ network: chainMock.chainName,
39
+ rpcUrls: {
40
+ default: {
41
+ http: [chainMock.rpcUrl],
42
+ },
43
+ public: {
44
+ http: [chainMock.rpcUrl],
45
+ },
46
+ },
47
+ ...(chainMock.utilityAddresses?.multicall && {
48
+ contracts: {
49
+ multicall3: {
50
+ address: chainMock.utilityAddresses.multicall,
51
+ },
52
+ },
53
+ }),
54
+ };
55
+
56
+ describe('client', () => {
57
+ const transportMock = { type: 'transport' };
58
+ const walletClientMock = { type: 'client', extend: jest.fn() };
59
+ const extendedWalletClientMock = { ...walletClientMock, extended: true };
60
+
61
+ beforeEach(() => {
62
+ jest.resetAllMocks();
63
+
64
+ walletClientMock.extend.mockReturnValueOnce(extendedWalletClientMock);
65
+
66
+ jest.mocked(http).mockReturnValueOnce(transportMock as unknown as HttpTransport);
67
+ jest.mocked(custom).mockReturnValueOnce(transportMock as unknown as CustomTransport);
68
+ jest.mocked(createWalletClient).mockReturnValueOnce(walletClientMock as unknown as WalletClient);
69
+ });
70
+
71
+ it('returns the client correctly without predefined provider', () => {
72
+ const client = getClientForChain({ chain: chainMock });
73
+ expect(client).toStrictEqual(extendedWalletClientMock);
74
+
75
+ expect(custom).not.toHaveBeenCalled();
76
+ expect(http).toHaveBeenCalledWith(chainMock.rpcUrl, { batch: true, retryCount: 0 });
77
+ expect(createWalletClient).toHaveBeenCalledWith({
78
+ chain: chainInfo,
79
+ transport: transportMock,
80
+ });
81
+ expect(walletClientMock.extend).toHaveBeenCalledWith(publicActions);
82
+ });
83
+
84
+ it('returns the client correctly with a predefined provider', () => {
85
+ const providerMock = { type: 'provider' } as unknown as Provider;
86
+ const client = getClientForChain({ chain: chainMock, provider: providerMock });
87
+ expect(client).toStrictEqual(extendedWalletClientMock);
88
+
89
+ expect(http).not.toHaveBeenCalled();
90
+ expect(custom).toHaveBeenCalledWith(providerMock);
91
+ expect(createWalletClient).toHaveBeenCalledWith({
92
+ chain: chainInfo,
93
+ transport: transportMock,
94
+ });
95
+ expect(walletClientMock.extend).toHaveBeenCalledWith(publicActions);
96
+ });
97
+ });
@@ -0,0 +1,44 @@
1
+ import { createWalletClient, publicActions, custom, http } from 'viem';
2
+ import type { Chain } from '../types/chain';
3
+ import type { Provider } from '../types';
4
+ import caip2 from './caip2';
5
+
6
+ const _getChain = (chain: Chain) => {
7
+ const { reference: chainId } = caip2.toJSON(chain.chainId);
8
+
9
+ return {
10
+ id: Number(chainId),
11
+ name: chain.chainName,
12
+ nativeCurrency: {
13
+ decimals: chain.networkToken.decimals,
14
+ symbol: chain.networkToken.symbol,
15
+ name: chain.networkToken.name,
16
+ },
17
+ network: chain.chainName,
18
+ rpcUrls: {
19
+ default: {
20
+ http: [chain.rpcUrl],
21
+ },
22
+ public: {
23
+ http: [chain.rpcUrl],
24
+ },
25
+ },
26
+ ...(chain.utilityAddresses?.multicall && {
27
+ contracts: {
28
+ multicall3: {
29
+ address: chain.utilityAddresses.multicall,
30
+ },
31
+ },
32
+ }),
33
+ };
34
+ };
35
+
36
+ export const getClientForChain = ({ chain, provider }: { chain: Chain; provider?: Provider }) => {
37
+ const chainInfo = _getChain(chain);
38
+ const transport = provider ? custom(provider) : http(chain.rpcUrl, { batch: true, retryCount: 0 });
39
+
40
+ return createWalletClient({
41
+ chain: chainInfo,
42
+ transport,
43
+ }).extend(publicActions);
44
+ };
@@ -0,0 +1,43 @@
1
+ import { BridgeInitializationError } from '../errors';
2
+ import { ErrorReason, type BridgeConfig, type BridgeService } from '../types';
3
+ import { ensureHasConfig } from './ensure-config';
4
+
5
+ describe('ensure-config', () => {
6
+ it('throws if it fails to update the config', async () => {
7
+ const bridgeMock = {
8
+ updateConfig: jest.fn(),
9
+ } as unknown as BridgeService;
10
+
11
+ await expect(ensureHasConfig(bridgeMock)).rejects.toThrow(
12
+ new BridgeInitializationError(ErrorReason.CONFIG_NOT_AVAILABLE),
13
+ );
14
+ expect(bridgeMock.updateConfig).toHaveBeenCalledTimes(1);
15
+ });
16
+
17
+ it('updates the config correctly', async () => {
18
+ const config = { foo: 'bar' } as unknown as BridgeConfig;
19
+ const bridgeMock = {
20
+ updateConfig: jest.fn().mockImplementation(() => {
21
+ bridgeMock.config = config;
22
+ }),
23
+ } as unknown as BridgeService;
24
+
25
+ expect(bridgeMock.config).toBeUndefined();
26
+ await expect(ensureHasConfig(bridgeMock)).resolves.toBeUndefined();
27
+ expect(bridgeMock.config).toStrictEqual(config);
28
+ expect(bridgeMock.updateConfig).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ it('does nothing if bridge already has config', async () => {
32
+ const config = { foo: 'bar' } as unknown as BridgeConfig;
33
+ const bridgeMock = {
34
+ config,
35
+ updateConfig: jest.fn(),
36
+ } as unknown as BridgeService;
37
+
38
+ expect(bridgeMock.config).toStrictEqual(config);
39
+ await expect(ensureHasConfig(bridgeMock)).resolves.toBeUndefined();
40
+ expect(bridgeMock.config).toStrictEqual(config);
41
+ expect(bridgeMock.updateConfig).not.toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,12 @@
1
+ import { BridgeInitializationError } from '../errors';
2
+ import { ErrorReason, type BridgeService } from '../types';
3
+
4
+ export async function ensureHasConfig(bridge: BridgeService) {
5
+ if (!bridge.config) {
6
+ await bridge.updateConfig();
7
+
8
+ if (!bridge.config) {
9
+ throw new BridgeInitializationError(ErrorReason.CONFIG_NOT_AVAILABLE);
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ export * from './bridge-types';
2
+ export { default as caip2 } from './caip2';
@@ -0,0 +1,24 @@
1
+ import type { Transaction, TransactionReceipt } from 'viem';
2
+ import { getNetworkFeeEVM } from './network-fee';
3
+
4
+ describe('network-fee', () => {
5
+ describe('getNetworkFeeEVM', () => {
6
+ const transactionMock = {
7
+ gasPrice: 50,
8
+ } as unknown as Transaction;
9
+
10
+ const receiptMock = {
11
+ gasUsed: 100,
12
+ } as unknown as TransactionReceipt;
13
+
14
+ it('returns undefined if gasPrice is missing', () => {
15
+ const networkFee = getNetworkFeeEVM({} as Transaction, receiptMock);
16
+ expect(networkFee).toBeUndefined();
17
+ });
18
+
19
+ it('returns the correct network fee', () => {
20
+ const networkFee = getNetworkFeeEVM(transactionMock, receiptMock);
21
+ expect(networkFee).toEqual(5000n);
22
+ });
23
+ });
24
+ });
@@ -0,0 +1,6 @@
1
+ import { type Transaction, type TransactionReceipt } from 'viem';
2
+
3
+ // returns the network fee for EVM transaction in wei (10**-18)
4
+ export const getNetworkFeeEVM = (transaction: Transaction, receipt: TransactionReceipt) => {
5
+ return transaction.gasPrice && BigInt(transaction.gasPrice * receipt.gasUsed);
6
+ };
@@ -0,0 +1,115 @@
1
+ import { noop } from 'lodash';
2
+ import { retryPromise } from './retry-promise';
3
+
4
+ describe('retry-promise', () => {
5
+ const delay = 5000;
6
+ const promiseMock = jest.fn();
7
+
8
+ beforeEach(() => {
9
+ jest.resetAllMocks();
10
+ jest.useFakeTimers();
11
+ });
12
+
13
+ it('rejects when cancelled', (done) => {
14
+ const test = async () => {
15
+ promiseMock.mockResolvedValue(undefined);
16
+
17
+ const { result, cancel } = retryPromise({ promise: promiseMock, delay });
18
+
19
+ for (let i = 1; i <= 5; i++) {
20
+ expect(promiseMock).toHaveBeenCalledTimes(i);
21
+ await jest.advanceTimersByTimeAsync(delay);
22
+ }
23
+
24
+ result.catch((err) => {
25
+ expect(err).toBe('cancelled');
26
+ done();
27
+ });
28
+
29
+ cancel();
30
+ };
31
+
32
+ test();
33
+ });
34
+
35
+ it('delays execution when `startAfter` is provided', (done) => {
36
+ const test = async () => {
37
+ const response = { foo: 'bar' };
38
+ const startAfter = 3000;
39
+
40
+ promiseMock
41
+ .mockResolvedValueOnce(undefined)
42
+ .mockResolvedValueOnce(undefined)
43
+ .mockImplementationOnce(async (done) => {
44
+ done(response);
45
+ });
46
+
47
+ const { result } = retryPromise({ promise: promiseMock, delay, startAfter });
48
+ expect(promiseMock).not.toHaveBeenCalled();
49
+
50
+ // initial delay
51
+ await jest.advanceTimersByTimeAsync(startAfter);
52
+ expect(promiseMock).toHaveBeenCalledTimes(1);
53
+
54
+ // normal execution
55
+ await jest.advanceTimersByTimeAsync(delay);
56
+ expect(promiseMock).toHaveBeenCalledTimes(2);
57
+
58
+ await jest.advanceTimersByTimeAsync(delay);
59
+ expect(promiseMock).toHaveBeenCalledTimes(3);
60
+
61
+ result.then((res) => {
62
+ expect(res).toStrictEqual(response);
63
+ done();
64
+ });
65
+ };
66
+
67
+ test();
68
+ });
69
+
70
+ it('uses exponential backoff after errors and resolves correctly', (done) => {
71
+ const test = async () => {
72
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(noop);
73
+ const response = { foo: 'bar' };
74
+
75
+ promiseMock
76
+ .mockResolvedValueOnce(undefined)
77
+ .mockRejectedValueOnce(new Error('error 1'))
78
+ .mockRejectedValueOnce(new Error('error 2'))
79
+ .mockRejectedValueOnce(new Error('error 3'))
80
+ .mockResolvedValueOnce(undefined)
81
+ .mockImplementationOnce(async (done) => {
82
+ done(response);
83
+ });
84
+
85
+ const { result } = retryPromise({ promise: promiseMock, delay });
86
+ expect(promiseMock).toHaveBeenCalledTimes(1);
87
+
88
+ // error 1
89
+ await jest.advanceTimersByTimeAsync(delay);
90
+ expect(promiseMock).toHaveBeenCalledTimes(2);
91
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, 'error 1');
92
+ // error 2
93
+ await jest.advanceTimersByTimeAsync(2 * delay);
94
+ expect(promiseMock).toHaveBeenCalledTimes(3);
95
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, 'error 2');
96
+ // error 3
97
+ await jest.advanceTimersByTimeAsync(4 * delay);
98
+ expect(promiseMock).toHaveBeenCalledTimes(4);
99
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, 'error 3');
100
+
101
+ await jest.advanceTimersByTimeAsync(8 * delay);
102
+ expect(promiseMock).toHaveBeenCalledTimes(5);
103
+
104
+ await jest.advanceTimersByTimeAsync(delay);
105
+ expect(promiseMock).toHaveBeenCalledTimes(6);
106
+
107
+ result.then((res) => {
108
+ expect(res).toStrictEqual(response);
109
+ done();
110
+ });
111
+ };
112
+
113
+ test();
114
+ });
115
+ });