@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,429 @@
1
+ import { encodeFunctionData, isAddress } from 'viem';
2
+ import {
3
+ Environment,
4
+ type BridgeService,
5
+ type TransferParams,
6
+ BridgeType,
7
+ ErrorReason,
8
+ type Signer,
9
+ BridgeSignatureReason,
10
+ } from '../../../types';
11
+ import { getClientForChain } from '../../../utils/client';
12
+ import { BRIDGE_ASSET } from '../__mocks__/asset.mock';
13
+ import { SOURCE_CHAIN, TARGET_CHAIN } from '../__mocks__/chain.mocks';
14
+ import { CCTP_CONFIG, SOURCE_ROUTER_ADDRESS } from '../__mocks__/config.mock';
15
+ import { transferAsset } from './transfer-asset';
16
+ import { getBridgeTransferMock } from '../__mocks__/bridge-transfer.mock';
17
+ import { ERC20_ABI } from '../abis/erc20';
18
+ import { TOKEN_ROUTER_ABI } from '../abis/token-router';
19
+ import { InvalidParamsError } from '../../../errors';
20
+
21
+ jest.mock('viem');
22
+ jest.mock('../../../utils/client');
23
+
24
+ describe('CCTP transferAsset', () => {
25
+ const environment = Environment.TEST;
26
+ const targetBlockNumber = 10n;
27
+
28
+ const sourceProviderMock = {
29
+ request: jest.fn(),
30
+ };
31
+ const targetProviderMock = {
32
+ request: jest.fn(),
33
+ };
34
+ const feesMock = {
35
+ [BRIDGE_ASSET.address!]: 2_000_000,
36
+ };
37
+ const bridgeMock = {
38
+ type: BridgeType.CCTP,
39
+ config: CCTP_CONFIG,
40
+ ensureHasConfig: jest.fn(),
41
+ getFees: jest.fn(),
42
+ } as unknown as BridgeService;
43
+ const sourceClientMock = {
44
+ readContract: jest.fn(),
45
+ writeContract: jest.fn(),
46
+ simulateContract: jest.fn(),
47
+ sendRawTransaction: jest.fn(),
48
+ waitForTransactionReceipt: jest.fn(),
49
+ };
50
+ const targetClientMock = {
51
+ getBlockNumber: jest.fn(),
52
+ };
53
+ const onStepChangeSpy = jest.fn();
54
+
55
+ const getTransferParams = (props?: Partial<TransferParams>) =>
56
+ ({
57
+ asset: BRIDGE_ASSET,
58
+ amount: 1_000_000,
59
+ fromAddress: '0x787',
60
+ sourceChain: SOURCE_CHAIN,
61
+ targetChain: TARGET_CHAIN,
62
+ sourceProvider: sourceProviderMock,
63
+ targetProvider: targetProviderMock,
64
+ onStepChange: onStepChangeSpy,
65
+ ...(props ?? {}),
66
+ }) as TransferParams;
67
+
68
+ beforeEach(() => {
69
+ jest.resetAllMocks();
70
+ targetClientMock.getBlockNumber.mockResolvedValue(targetBlockNumber);
71
+ (bridgeMock.getFees as jest.Mock).mockResolvedValue(feesMock);
72
+ (getClientForChain as jest.Mock).mockReturnValueOnce(sourceClientMock).mockReturnValueOnce(targetClientMock);
73
+ jest.mocked(isAddress).mockReturnValue(true);
74
+ });
75
+
76
+ it('calls ensureHasConfig', async () => {
77
+ const params = getTransferParams();
78
+ const error = new Error('error');
79
+ (bridgeMock.ensureHasConfig as jest.Mock).mockRejectedValueOnce(error);
80
+
81
+ await expect(transferAsset(bridgeMock, params, environment)).rejects.toThrow(error);
82
+ });
83
+
84
+ it('throws when addresses are incorrect', async () => {
85
+ jest.mocked(isAddress).mockReturnValue(false);
86
+ const params = getTransferParams();
87
+ await expect(transferAsset(bridgeMock, params, environment)).rejects.toThrow(
88
+ new InvalidParamsError(ErrorReason.INCORRECT_ADDRESS_PROVIDED),
89
+ );
90
+ });
91
+
92
+ describe('needs additional approval', () => {
93
+ it('works with a provided signer', async () => {
94
+ const start = 1000;
95
+ const dateSpy = jest.spyOn(Date, 'now');
96
+ dateSpy.mockReturnValue(start);
97
+
98
+ const approvalSignedHex = '0x1';
99
+ const approvalTxHash = '0x2';
100
+ const approvalTxData = 'approval-tx-data';
101
+ const bridgeSignedHex = '0x3';
102
+ const bridgeTxHash = '0x4';
103
+ const bridgeTxData = 'bridge-tx-data';
104
+
105
+ const approvalSignMock: Signer = jest.fn((_, dispatch) => dispatch(approvalSignedHex));
106
+ const bridgeSignMock: Signer = jest.fn((_, dispatch) => dispatch(bridgeSignedHex));
107
+ const customSigner = jest.fn().mockImplementationOnce(approvalSignMock).mockImplementationOnce(bridgeSignMock);
108
+
109
+ sourceClientMock.readContract.mockResolvedValue(0n);
110
+ sourceClientMock.sendRawTransaction.mockResolvedValueOnce(approvalTxHash).mockResolvedValueOnce(bridgeTxHash);
111
+ (encodeFunctionData as jest.Mock).mockReturnValueOnce(approvalTxData).mockReturnValueOnce(bridgeTxData);
112
+
113
+ const params = getTransferParams({ sourceProvider: sourceProviderMock, sign: customSigner });
114
+ const bridgeTransfer = await transferAsset(bridgeMock, params, environment);
115
+
116
+ expect(bridgeTransfer).toStrictEqual(
117
+ getBridgeTransferMock({
118
+ amount: params.amount,
119
+ amountDecimals: params.asset.decimals,
120
+ fromAddress: params.fromAddress,
121
+ sourceTxHash: bridgeTxHash,
122
+ startBlockNumber: targetBlockNumber,
123
+ sourceStartedAt: start,
124
+ }),
125
+ );
126
+
127
+ expect(getClientForChain).toHaveBeenCalledTimes(2);
128
+ expect(getClientForChain).toHaveBeenNthCalledWith(1, {
129
+ chain: params.sourceChain,
130
+ provider: sourceProviderMock,
131
+ });
132
+ expect(getClientForChain).toHaveBeenNthCalledWith(2, {
133
+ chain: params.targetChain,
134
+ provider: targetProviderMock,
135
+ });
136
+
137
+ expect(sourceClientMock.readContract).toHaveBeenCalledWith({
138
+ address: BRIDGE_ASSET.address,
139
+ abi: ERC20_ABI,
140
+ functionName: 'allowance',
141
+ args: [params.fromAddress, SOURCE_ROUTER_ADDRESS],
142
+ });
143
+
144
+ expect(encodeFunctionData).toHaveBeenCalledTimes(2);
145
+ expect(encodeFunctionData).toHaveBeenNthCalledWith(1, {
146
+ abi: ERC20_ABI,
147
+ functionName: 'approve',
148
+ args: [SOURCE_ROUTER_ADDRESS, params.amount],
149
+ });
150
+ expect(encodeFunctionData).toHaveBeenNthCalledWith(2, {
151
+ abi: TOKEN_ROUTER_ABI,
152
+ functionName: 'transferTokens',
153
+ args: [params.amount, 1, params.fromAddress, BRIDGE_ASSET.address],
154
+ });
155
+
156
+ expect(customSigner).toHaveBeenCalledTimes(2);
157
+ expect(customSigner).toHaveBeenNthCalledWith(
158
+ 1,
159
+ {
160
+ from: params.fromAddress,
161
+ to: BRIDGE_ASSET.address,
162
+ data: approvalTxData,
163
+ },
164
+ expect.any(Function),
165
+ );
166
+ expect(customSigner).toHaveBeenNthCalledWith(
167
+ 2,
168
+ {
169
+ from: params.fromAddress,
170
+ to: SOURCE_ROUTER_ADDRESS,
171
+ data: bridgeTxData,
172
+ },
173
+ expect.any(Function),
174
+ );
175
+
176
+ expect(sourceClientMock.sendRawTransaction).toHaveBeenCalledTimes(2);
177
+ expect(sourceClientMock.sendRawTransaction).toHaveBeenNthCalledWith(1, {
178
+ serializedTransaction: approvalSignedHex,
179
+ });
180
+ expect(sourceClientMock.sendRawTransaction).toHaveBeenNthCalledWith(2, {
181
+ serializedTransaction: bridgeSignedHex,
182
+ });
183
+
184
+ expect(sourceClientMock.waitForTransactionReceipt).toHaveBeenCalledWith({
185
+ hash: approvalTxHash,
186
+ pollingInterval: 1_000,
187
+ });
188
+
189
+ expect(onStepChangeSpy).toHaveBeenCalledTimes(2);
190
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(1, {
191
+ currentSignature: 1,
192
+ requiredSignatures: 2,
193
+ currentSignatureReason: BridgeSignatureReason.AllowanceApproval,
194
+ });
195
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(2, {
196
+ currentSignature: 2,
197
+ requiredSignatures: 2,
198
+ currentSignatureReason: BridgeSignatureReason.TokensTransfer,
199
+ });
200
+ });
201
+
202
+ it('works without provided signer', async () => {
203
+ const start = 1000;
204
+ const dateSpy = jest.spyOn(Date, 'now');
205
+ dateSpy.mockReturnValue(start);
206
+
207
+ const approvalTxHash = '0x1';
208
+ const approvalTxRequest = 'approval-tx-request';
209
+ const bridgeTxHash = '0x2';
210
+ const bridgeTxRequest = 'bridge-tx-request';
211
+
212
+ sourceClientMock.readContract.mockResolvedValue(0n);
213
+ sourceClientMock.simulateContract
214
+ .mockResolvedValueOnce({ request: approvalTxRequest })
215
+ .mockResolvedValueOnce({ request: bridgeTxRequest });
216
+ sourceClientMock.writeContract.mockResolvedValueOnce(approvalTxHash).mockResolvedValueOnce(bridgeTxHash);
217
+
218
+ const params = getTransferParams({ sourceProvider: sourceProviderMock });
219
+ const bridgeTransfer = await transferAsset(bridgeMock, params, environment);
220
+
221
+ expect(bridgeTransfer).toStrictEqual(
222
+ getBridgeTransferMock({
223
+ amount: params.amount,
224
+ amountDecimals: params.asset.decimals,
225
+ fromAddress: params.fromAddress,
226
+ sourceTxHash: bridgeTxHash,
227
+ startBlockNumber: targetBlockNumber,
228
+ sourceStartedAt: start,
229
+ }),
230
+ );
231
+
232
+ expect(getClientForChain).toHaveBeenCalledTimes(2);
233
+ expect(getClientForChain).toHaveBeenNthCalledWith(1, {
234
+ chain: params.sourceChain,
235
+ provider: sourceProviderMock,
236
+ });
237
+ expect(getClientForChain).toHaveBeenNthCalledWith(2, {
238
+ chain: params.targetChain,
239
+ provider: targetProviderMock,
240
+ });
241
+
242
+ expect(sourceClientMock.readContract).toHaveBeenCalledWith({
243
+ address: BRIDGE_ASSET.address,
244
+ abi: ERC20_ABI,
245
+ functionName: 'allowance',
246
+ args: [params.fromAddress, SOURCE_ROUTER_ADDRESS],
247
+ });
248
+
249
+ expect(sourceClientMock.simulateContract).toHaveBeenCalledTimes(2);
250
+ expect(sourceClientMock.simulateContract).toHaveBeenNthCalledWith(1, {
251
+ account: params.fromAddress,
252
+ address: BRIDGE_ASSET.address,
253
+ abi: ERC20_ABI,
254
+ functionName: 'approve',
255
+ args: [SOURCE_ROUTER_ADDRESS, params.amount],
256
+ });
257
+ expect(sourceClientMock.simulateContract).toHaveBeenNthCalledWith(2, {
258
+ account: params.fromAddress,
259
+ address: SOURCE_ROUTER_ADDRESS,
260
+ abi: TOKEN_ROUTER_ABI,
261
+ functionName: 'transferTokens',
262
+ args: [params.amount, 1, params.fromAddress, BRIDGE_ASSET.address],
263
+ });
264
+
265
+ expect(sourceClientMock.writeContract).toHaveBeenCalledTimes(2);
266
+ expect(sourceClientMock.writeContract).toHaveBeenNthCalledWith(1, approvalTxRequest);
267
+ expect(sourceClientMock.writeContract).toHaveBeenNthCalledWith(2, bridgeTxRequest);
268
+
269
+ expect(sourceClientMock.waitForTransactionReceipt).toHaveBeenCalledWith({
270
+ hash: approvalTxHash,
271
+ pollingInterval: 1_000,
272
+ });
273
+
274
+ expect(onStepChangeSpy).toHaveBeenCalledTimes(2);
275
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(1, {
276
+ currentSignature: 1,
277
+ requiredSignatures: 2,
278
+ currentSignatureReason: BridgeSignatureReason.AllowanceApproval,
279
+ });
280
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(2, {
281
+ currentSignature: 2,
282
+ requiredSignatures: 2,
283
+ currentSignatureReason: BridgeSignatureReason.TokensTransfer,
284
+ });
285
+ });
286
+ });
287
+
288
+ describe('does not need additional approval', () => {
289
+ it('works with a provided signer', async () => {
290
+ const start = 1000;
291
+ const dateSpy = jest.spyOn(Date, 'now');
292
+ dateSpy.mockReturnValue(start);
293
+
294
+ const bridgeSignedHex = '0x3';
295
+ const bridgeTxHash = '0x4';
296
+ const bridgeTxData = 'bridge-tx-data';
297
+
298
+ const bridgeSignMock: Signer = jest.fn((_, dispatch) => dispatch(bridgeSignedHex));
299
+ const customSigner = jest.fn().mockImplementationOnce(bridgeSignMock);
300
+ const params = getTransferParams({ sourceProvider: sourceProviderMock, sign: customSigner });
301
+
302
+ sourceClientMock.readContract.mockResolvedValue(params.amount);
303
+ sourceClientMock.sendRawTransaction.mockResolvedValueOnce(bridgeTxHash);
304
+ (encodeFunctionData as jest.Mock).mockReturnValueOnce(bridgeTxData);
305
+
306
+ const bridgeTransfer = await transferAsset(bridgeMock, params, environment);
307
+
308
+ expect(bridgeTransfer).toStrictEqual(
309
+ getBridgeTransferMock({
310
+ amount: params.amount,
311
+ amountDecimals: params.asset.decimals,
312
+ fromAddress: params.fromAddress,
313
+ sourceTxHash: bridgeTxHash,
314
+ startBlockNumber: targetBlockNumber,
315
+ sourceStartedAt: start,
316
+ }),
317
+ );
318
+
319
+ expect(getClientForChain).toHaveBeenCalledTimes(2);
320
+ expect(getClientForChain).toHaveBeenNthCalledWith(1, {
321
+ chain: params.sourceChain,
322
+ provider: sourceProviderMock,
323
+ });
324
+ expect(getClientForChain).toHaveBeenNthCalledWith(2, {
325
+ chain: params.targetChain,
326
+ provider: targetProviderMock,
327
+ });
328
+
329
+ expect(sourceClientMock.readContract).toHaveBeenCalledWith({
330
+ address: BRIDGE_ASSET.address,
331
+ abi: ERC20_ABI,
332
+ functionName: 'allowance',
333
+ args: [params.fromAddress, SOURCE_ROUTER_ADDRESS],
334
+ });
335
+
336
+ expect(encodeFunctionData).toHaveBeenCalledTimes(1);
337
+ expect(encodeFunctionData).toHaveBeenCalledWith({
338
+ abi: TOKEN_ROUTER_ABI,
339
+ functionName: 'transferTokens',
340
+ args: [params.amount, 1, params.fromAddress, BRIDGE_ASSET.address],
341
+ });
342
+
343
+ expect(customSigner).toHaveBeenCalledTimes(1);
344
+ expect(customSigner).toHaveBeenCalledWith(
345
+ {
346
+ from: params.fromAddress,
347
+ to: SOURCE_ROUTER_ADDRESS,
348
+ data: bridgeTxData,
349
+ },
350
+ expect.any(Function),
351
+ );
352
+
353
+ expect(sourceClientMock.sendRawTransaction).toHaveBeenCalledTimes(1);
354
+ expect(sourceClientMock.sendRawTransaction).toHaveBeenCalledWith({
355
+ serializedTransaction: bridgeSignedHex,
356
+ });
357
+
358
+ expect(onStepChangeSpy).toHaveBeenCalledTimes(1);
359
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(1, {
360
+ currentSignature: 1,
361
+ requiredSignatures: 1,
362
+ currentSignatureReason: BridgeSignatureReason.TokensTransfer,
363
+ });
364
+ });
365
+
366
+ it('works without provided signer', async () => {
367
+ const start = 1000;
368
+ const dateSpy = jest.spyOn(Date, 'now');
369
+ dateSpy.mockReturnValue(start);
370
+
371
+ const bridgeTxHash = '0x2';
372
+ const bridgeTxRequest = 'bridge-tx-request';
373
+ const params = getTransferParams({ sourceProvider: sourceProviderMock });
374
+
375
+ sourceClientMock.readContract.mockResolvedValue(params.amount);
376
+ sourceClientMock.simulateContract.mockResolvedValueOnce({ request: bridgeTxRequest });
377
+ sourceClientMock.writeContract.mockResolvedValueOnce(bridgeTxHash);
378
+
379
+ const bridgeTransfer = await transferAsset(bridgeMock, params, environment);
380
+
381
+ expect(bridgeTransfer).toStrictEqual(
382
+ getBridgeTransferMock({
383
+ amount: params.amount,
384
+ amountDecimals: params.asset.decimals,
385
+ fromAddress: params.fromAddress,
386
+ sourceTxHash: bridgeTxHash,
387
+ startBlockNumber: targetBlockNumber,
388
+ sourceStartedAt: start,
389
+ }),
390
+ );
391
+
392
+ expect(getClientForChain).toHaveBeenCalledTimes(2);
393
+ expect(getClientForChain).toHaveBeenNthCalledWith(1, {
394
+ chain: params.sourceChain,
395
+ provider: sourceProviderMock,
396
+ });
397
+ expect(getClientForChain).toHaveBeenNthCalledWith(2, {
398
+ chain: params.targetChain,
399
+ provider: targetProviderMock,
400
+ });
401
+
402
+ expect(sourceClientMock.readContract).toHaveBeenCalledWith({
403
+ address: BRIDGE_ASSET.address,
404
+ abi: ERC20_ABI,
405
+ functionName: 'allowance',
406
+ args: [params.fromAddress, SOURCE_ROUTER_ADDRESS],
407
+ });
408
+
409
+ expect(sourceClientMock.simulateContract).toHaveBeenCalledTimes(1);
410
+ expect(sourceClientMock.simulateContract).toHaveBeenCalledWith({
411
+ account: params.fromAddress,
412
+ address: SOURCE_ROUTER_ADDRESS,
413
+ abi: TOKEN_ROUTER_ABI,
414
+ functionName: 'transferTokens',
415
+ args: [params.amount, 1, params.fromAddress, BRIDGE_ASSET.address],
416
+ });
417
+
418
+ expect(sourceClientMock.writeContract).toHaveBeenCalledTimes(1);
419
+ expect(sourceClientMock.writeContract).toHaveBeenCalledWith(bridgeTxRequest);
420
+
421
+ expect(onStepChangeSpy).toHaveBeenCalledTimes(1);
422
+ expect(onStepChangeSpy).toHaveBeenNthCalledWith(1, {
423
+ currentSignature: 1,
424
+ requiredSignatures: 1,
425
+ currentSignatureReason: BridgeSignatureReason.TokensTransfer,
426
+ });
427
+ });
428
+ });
429
+ });
@@ -0,0 +1,179 @@
1
+ import { encodeFunctionData, isAddress, type PublicClient } from 'viem';
2
+ import {
3
+ ErrorReason,
4
+ type BridgeService,
5
+ type Environment,
6
+ type TransferParams,
7
+ type Hex,
8
+ type BridgeTransfer,
9
+ BridgeSignatureReason,
10
+ } from '../../../types';
11
+ import { getClientForChain } from '../../../utils/client';
12
+ import { ERC20_ABI } from '../abis/erc20';
13
+ import { getTransferData } from '../utils/transfer-data';
14
+ import { TOKEN_ROUTER_ABI } from '../abis/token-router';
15
+ import { InvalidParamsError } from '../../../errors';
16
+
17
+ const approveAndTransfer = async (bridge: BridgeService, params: TransferParams) => {
18
+ const {
19
+ sourceChain,
20
+ targetChain,
21
+ asset,
22
+ amount,
23
+ fromAddress,
24
+ toAddress: maybeToAddress,
25
+ sourceProvider,
26
+ onStepChange,
27
+ sign,
28
+ } = params;
29
+ const toAddress = maybeToAddress ?? fromAddress;
30
+
31
+ if (!isAddress(fromAddress) || !isAddress(toAddress)) {
32
+ throw new InvalidParamsError(ErrorReason.INCORRECT_ADDRESS_PROVIDED);
33
+ }
34
+
35
+ const { sourceChainData, targetChainData, burnToken } = getTransferData(
36
+ { sourceChain, targetChain, asset, amount },
37
+ bridge.config!,
38
+ );
39
+ const client = getClientForChain({ chain: sourceChain, provider: sourceProvider });
40
+
41
+ const allowance = await client.readContract({
42
+ address: burnToken.address,
43
+ abi: ERC20_ABI,
44
+ functionName: 'allowance',
45
+ args: [fromAddress, sourceChainData.tokenRouterAddress],
46
+ });
47
+
48
+ const isAllowanceApprovalRequired = allowance < amount;
49
+ const requiredSignatures = isAllowanceApprovalRequired ? 2 : 1; // if approval is required, we'll need 2 signatures
50
+
51
+ if (isAllowanceApprovalRequired) {
52
+ onStepChange?.({
53
+ currentSignature: 1,
54
+ currentSignatureReason: BridgeSignatureReason.AllowanceApproval,
55
+ requiredSignatures,
56
+ });
57
+
58
+ if (sign) {
59
+ const data = encodeFunctionData({
60
+ abi: ERC20_ABI,
61
+ functionName: 'approve',
62
+ args: [sourceChainData.tokenRouterAddress, amount],
63
+ });
64
+
65
+ const txHash = await sign(
66
+ {
67
+ from: fromAddress,
68
+ to: burnToken.address,
69
+ data,
70
+ },
71
+ (signedTxHash: Hex) => client.sendRawTransaction({ serializedTransaction: signedTxHash }),
72
+ );
73
+
74
+ await client.waitForTransactionReceipt({ hash: txHash, pollingInterval: 1_000 });
75
+ } else {
76
+ const { request } = await client.simulateContract({
77
+ account: fromAddress,
78
+ address: burnToken.address,
79
+ abi: ERC20_ABI,
80
+ functionName: 'approve',
81
+ args: [sourceChainData.tokenRouterAddress, amount],
82
+ });
83
+
84
+ const txHash = await client.writeContract(request);
85
+ await client.waitForTransactionReceipt({ hash: txHash, pollingInterval: 1_000 });
86
+ }
87
+ }
88
+
89
+ onStepChange?.({
90
+ currentSignature: isAllowanceApprovalRequired ? 2 : 1,
91
+ currentSignatureReason: BridgeSignatureReason.TokensTransfer,
92
+ requiredSignatures,
93
+ });
94
+
95
+ if (sign) {
96
+ const data = encodeFunctionData({
97
+ abi: TOKEN_ROUTER_ABI,
98
+ functionName: 'transferTokens',
99
+ args: [amount, targetChainData.domain, toAddress, burnToken.address],
100
+ });
101
+
102
+ return sign(
103
+ {
104
+ from: fromAddress,
105
+ to: sourceChainData.tokenRouterAddress,
106
+ data,
107
+ },
108
+ (signedTxHash: Hex) => client.sendRawTransaction({ serializedTransaction: signedTxHash }),
109
+ );
110
+ } else {
111
+ const { request } = await client.simulateContract({
112
+ account: fromAddress,
113
+ address: sourceChainData.tokenRouterAddress,
114
+ abi: TOKEN_ROUTER_ABI,
115
+ functionName: 'transferTokens',
116
+ args: [amount, targetChainData.domain, toAddress, burnToken.address],
117
+ });
118
+
119
+ return client.writeContract(request);
120
+ }
121
+ };
122
+
123
+ const getStartBlockNumber = async (targetClient: PublicClient) => {
124
+ try {
125
+ const startBlockNumber = await targetClient.getBlockNumber();
126
+ return startBlockNumber;
127
+ } catch {
128
+ return undefined;
129
+ }
130
+ };
131
+
132
+ export async function transferAsset(
133
+ bridge: BridgeService,
134
+ params: TransferParams,
135
+ environment: Environment,
136
+ ): Promise<BridgeTransfer> {
137
+ await bridge.ensureHasConfig();
138
+
139
+ const { minimumConfirmations: requiredSourceConfirmationCount } =
140
+ bridge.config!.find((chainData) => chainData.chainId === params.sourceChain.chainId) ?? {};
141
+ const { minimumConfirmations: requiredTargetConfirmationCount } =
142
+ bridge.config!.find((chainData) => chainData.chainId === params.targetChain.chainId) ?? {};
143
+
144
+ if (!requiredSourceConfirmationCount || !requiredTargetConfirmationCount) {
145
+ throw new InvalidParamsError(ErrorReason.CONFIRMATION_COUNT_UNKNOWN);
146
+ }
147
+
148
+ const fees = await bridge.getFees({ ...params, provider: params.sourceProvider });
149
+
150
+ const bridgeFee = (params.asset.address && fees[params.asset.address]) ?? 0n;
151
+ const txHash = await approveAndTransfer(bridge, params);
152
+ const sourceStartedAt = Date.now();
153
+ const targetClient = getClientForChain({ chain: params.targetChain, provider: params.targetProvider });
154
+ const targetBlockNumber = await getStartBlockNumber(targetClient);
155
+
156
+ return {
157
+ type: bridge.type,
158
+ environment,
159
+ fromAddress: params.fromAddress,
160
+ toAddress: params.toAddress ?? params.fromAddress,
161
+ amount: params.amount,
162
+ amountDecimals: params.asset.decimals,
163
+ symbol: params.asset.symbol,
164
+
165
+ bridgeFee,
166
+
167
+ sourceChain: params.sourceChain,
168
+ sourceStartedAt,
169
+ sourceTxHash: txHash,
170
+ sourceConfirmationCount: 0,
171
+ requiredSourceConfirmationCount,
172
+
173
+ targetChain: params.targetChain,
174
+ targetConfirmationCount: 0,
175
+ requiredTargetConfirmationCount,
176
+
177
+ startBlockNumber: targetBlockNumber,
178
+ };
179
+ }
@@ -0,0 +1 @@
1
+ export * from './factory';
@@ -0,0 +1,4 @@
1
+ export enum AvalancheChainIds {
2
+ FUJI = 'eip155:43113',
3
+ MAINNET = 'eip155:43114',
4
+ }
@@ -0,0 +1,19 @@
1
+ import type { Address } from 'viem';
2
+
3
+ type Token = {
4
+ address: Address;
5
+ name: string;
6
+ symbol: string;
7
+ decimals: number;
8
+ };
9
+
10
+ type ChainData = {
11
+ chainId: string;
12
+ domain: number;
13
+ tokenRouterAddress: Address;
14
+ messageTransmitterAddress: Address;
15
+ tokens: Token[];
16
+ minimumConfirmations: number;
17
+ };
18
+
19
+ export type Config = ChainData[];
@@ -0,0 +1,49 @@
1
+ import { BridgeInitializationError } from '../../../errors';
2
+ import { Environment, ErrorReason } from '../../../types';
3
+ import { AvalancheChainIds } from '../types/chain';
4
+ import { getConfig, getTrackingDelayByChainId } from './config';
5
+
6
+ describe('CCTP config', () => {
7
+ describe('getConfig', () => {
8
+ let fetchSpy: jest.SpyInstance;
9
+
10
+ const config = [{ chainId: 1 }];
11
+ const environment = Environment.TEST;
12
+
13
+ beforeEach(() => {
14
+ jest.restoreAllMocks();
15
+ fetchSpy = jest.spyOn(globalThis, 'fetch');
16
+ });
17
+
18
+ it('throws when fetching config fails', async () => {
19
+ const error = new Error('some error');
20
+ fetchSpy.mockRejectedValueOnce(error);
21
+
22
+ await expect(getConfig(environment)).rejects.toThrow(
23
+ new BridgeInitializationError(
24
+ ErrorReason.CONFIG_NOT_AVAILABLE,
25
+ `Error while fetching CCTP config: ${error.message}`,
26
+ ),
27
+ );
28
+ expect(fetchSpy).toHaveBeenCalledWith(expect.any(String));
29
+ });
30
+
31
+ it('returns the correct config', async () => {
32
+ fetchSpy.mockResolvedValue({ json: jest.fn().mockResolvedValue(config) } as unknown as Response);
33
+ const result = await getConfig(environment);
34
+
35
+ expect(result).toStrictEqual([{ chainId: 'eip155:1' }]);
36
+ expect(fetchSpy).toHaveBeenCalledWith(expect.any(String));
37
+ });
38
+ });
39
+
40
+ describe('getTrackingDelayByChainId', () => {
41
+ it.each([
42
+ { name: 'avalanche', chainId: AvalancheChainIds.MAINNET, expectedDelay: 1000 },
43
+ { name: 'avalanche fuji', chainId: AvalancheChainIds.FUJI, expectedDelay: 1000 },
44
+ { name: 'unknown', chainId: 'eip155:999', expectedDelay: 20000 },
45
+ ])('returns the correct delay for $name', ({ chainId, expectedDelay }) => {
46
+ expect(getTrackingDelayByChainId(chainId)).toBe(expectedDelay);
47
+ });
48
+ });
49
+ });