@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,779 @@
1
+ import { decodeEventLog } from 'viem';
2
+ import { BridgeType, type BridgeService, ErrorCode, ErrorReason } from '../../../types';
3
+ import { getClientForChain } from '../../../utils/client';
4
+ import { getNetworkFeeEVM } from '../../../utils/network-fee';
5
+ import { retryPromise } from '../../../utils/retry-promise';
6
+ import { getBridgeTrackinParams } from '../__mocks__/bridge-transfer.mock';
7
+ import {
8
+ CCTP_CONFIG,
9
+ SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
10
+ TARGET_TRANSMITTER_ADDRESS,
11
+ } from '../__mocks__/config.mock';
12
+ import { getTrackingDelayByChainId } from '../utils/config';
13
+ import * as tracking from './track-transfer';
14
+ import {
15
+ MAX_BLOCKS,
16
+ TRACKING_LIMIT_MS,
17
+ INITIAL_DELAY,
18
+ trackSourceTx,
19
+ trackTargetTx,
20
+ trackTransfer,
21
+ } from './track-transfer';
22
+ import { SOURCE_CHAIN, TARGET_CHAIN } from '../__mocks__/chain.mocks';
23
+ import { TOKEN_ROUTER_ABI } from '../abis/token-router';
24
+ import { InvalidParamsError } from '../../../errors';
25
+
26
+ jest.mock('../../../utils/retry-promise');
27
+ jest.mock('../../../utils/client');
28
+ jest.mock('../../../utils/network-fee');
29
+ jest.mock('../utils/config');
30
+ jest.mock('viem');
31
+
32
+ describe('CCTP trackTransfer', () => {
33
+ const now = 10;
34
+ const networkFee = 1000n;
35
+ const delay = 5000;
36
+
37
+ // used for updateListener callback param testing
38
+ // https://github.com/jestjs/jest/issues/434
39
+ const updateListenerWithoutReference = jest.fn();
40
+ const updateListener = jest.fn();
41
+ const doneMock = jest.fn();
42
+
43
+ const txMock = { hash: 'hash' };
44
+ const bridgeMock = {
45
+ type: BridgeType.CCTP,
46
+ config: CCTP_CONFIG,
47
+ ensureHasConfig: jest.fn(),
48
+ } as unknown as BridgeService;
49
+ const sourceClientMock = {
50
+ getTransactionReceipt: jest.fn(),
51
+ getTransaction: jest.fn(),
52
+ getTransactionConfirmations: jest.fn(),
53
+ };
54
+ const targetClientMock = {
55
+ getTransactionReceipt: jest.fn(),
56
+ getTransaction: jest.fn(),
57
+ getTransactionConfirmations: jest.fn(),
58
+ getBlockNumber: jest.fn(),
59
+ getLogs: jest.fn(),
60
+ };
61
+
62
+ beforeEach(() => {
63
+ jest.resetAllMocks();
64
+ jest.useFakeTimers().setSystemTime(new Date(now));
65
+
66
+ (retryPromise as jest.Mock).mockImplementation(
67
+ ({ promise }: { promise: (done: () => unknown) => Promise<unknown> }) => ({
68
+ result: promise(doneMock),
69
+ }),
70
+ );
71
+
72
+ updateListener.mockImplementation((args) => updateListenerWithoutReference({ ...args }));
73
+ doneMock.mockImplementation((arg) => arg);
74
+ sourceClientMock.getTransaction.mockResolvedValue(txMock);
75
+ targetClientMock.getTransaction.mockResolvedValue(txMock);
76
+ (getNetworkFeeEVM as jest.Mock).mockReturnValue(networkFee);
77
+ (getTrackingDelayByChainId as jest.Mock).mockReturnValue(delay);
78
+ });
79
+
80
+ afterEach(() => {
81
+ jest.useRealTimers();
82
+ });
83
+
84
+ describe('combined tracking', () => {
85
+ afterEach(() => {
86
+ jest.restoreAllMocks();
87
+ });
88
+
89
+ it('calls ensureHasConfig', async () => {
90
+ const params = getBridgeTrackinParams({ updateListener });
91
+ const error = new Error('error');
92
+ (bridgeMock.ensureHasConfig as jest.Mock).mockRejectedValueOnce(error);
93
+
94
+ const { result } = trackTransfer(bridgeMock, params);
95
+
96
+ await expect(result).rejects.toThrow(error);
97
+ });
98
+
99
+ it('tracks the transactions correctly', async () => {
100
+ const initialParams = getBridgeTrackinParams({ updateListener });
101
+ const transferAfterSource = { ...initialParams.bridgeTransfer, metadata: { nonce: 1 } };
102
+ const transferAfterTarget = { ...transferAfterSource, completedAt: 999 };
103
+
104
+ const sourceCancel = jest.fn();
105
+ const targetCancel = jest.fn();
106
+
107
+ const sourceTrackerSpy = jest.spyOn(tracking, 'trackSourceTx').mockResolvedValueOnce({
108
+ result: new Promise((res) => res(transferAfterSource)),
109
+ cancel: sourceCancel,
110
+ });
111
+
112
+ const targetTrackerSpy = jest.spyOn(tracking, 'trackTargetTx').mockResolvedValueOnce({
113
+ result: new Promise((res) => res(transferAfterTarget)),
114
+ cancel: targetCancel,
115
+ });
116
+
117
+ const { result, cancel } = trackTransfer(bridgeMock, initialParams);
118
+ expect(await result).toStrictEqual(transferAfterTarget);
119
+ expect(sourceTrackerSpy).toHaveBeenCalledWith(bridgeMock.config, initialParams);
120
+ expect(targetTrackerSpy).toHaveBeenCalledWith(bridgeMock.config, {
121
+ ...initialParams,
122
+ bridgeTransfer: transferAfterSource,
123
+ });
124
+
125
+ cancel();
126
+ expect(sourceCancel).not.toHaveBeenCalled();
127
+ expect(targetCancel).toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ describe('source tx tracking', () => {
132
+ beforeEach(() => {
133
+ (getClientForChain as jest.Mock)
134
+ .mockReturnValueOnce(sourceClientMock)
135
+ .mockReturnValueOnce(targetClientMock)
136
+ .mockReturnValueOnce(targetClientMock);
137
+ });
138
+
139
+ it('resolves if bridging has already completed', async () => {
140
+ const params = getBridgeTrackinParams({
141
+ bridgeTransfer: {
142
+ completedAt: 1,
143
+ },
144
+ updateListener,
145
+ });
146
+
147
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
148
+ expect(await result).toStrictEqual(params.bridgeTransfer);
149
+ expect(doneMock).toHaveBeenCalledWith(params.bridgeTransfer);
150
+ expect(updateListenerWithoutReference).not.toHaveBeenCalled();
151
+
152
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
153
+ expect(retryPromise).toHaveBeenCalledWith({
154
+ promise: expect.any(Function),
155
+ delay,
156
+ startAfter: INITIAL_DELAY,
157
+ });
158
+ });
159
+
160
+ it('resolves if message nonce is already known', async () => {
161
+ const params = getBridgeTrackinParams({
162
+ bridgeTransfer: {
163
+ metadata: { nonce: 1 },
164
+ },
165
+ updateListener,
166
+ });
167
+
168
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
169
+ expect(await result).toStrictEqual(params.bridgeTransfer);
170
+ expect(doneMock).toHaveBeenCalledWith(params.bridgeTransfer);
171
+ expect(updateListenerWithoutReference).not.toHaveBeenCalled();
172
+
173
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
174
+ expect(retryPromise).toHaveBeenCalledWith({
175
+ promise: expect.any(Function),
176
+ delay,
177
+ startAfter: INITIAL_DELAY,
178
+ });
179
+ });
180
+
181
+ it('resolves if transaction has timed out', async () => {
182
+ const params = getBridgeTrackinParams({
183
+ updateListener,
184
+ });
185
+ const time = params.bridgeTransfer.sourceStartedAt + TRACKING_LIMIT_MS;
186
+
187
+ jest.useFakeTimers().setSystemTime(time);
188
+
189
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
190
+ const expectedResult = { ...params.bridgeTransfer, completedAt: time, errorCode: ErrorCode.TIMEOUT };
191
+
192
+ expect(await result).toStrictEqual(expectedResult);
193
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
194
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(1);
195
+ expect(updateListenerWithoutReference).toHaveBeenCalledWith(expectedResult);
196
+
197
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
198
+ expect(retryPromise).toHaveBeenCalledWith({
199
+ promise: expect.any(Function),
200
+ delay,
201
+ startAfter: INITIAL_DELAY,
202
+ });
203
+ });
204
+
205
+ it('resolves if transaction was reverted', async () => {
206
+ const receiptMock = {
207
+ status: 'reverted',
208
+ };
209
+ const params = getBridgeTrackinParams({
210
+ updateListener,
211
+ });
212
+
213
+ sourceClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
214
+
215
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
216
+ const expectedResult = {
217
+ ...params.bridgeTransfer,
218
+ sourceNetworkFee: networkFee,
219
+ completedAt: now,
220
+ errorCode: ErrorCode.TRANSACTION_REVERTED,
221
+ };
222
+
223
+ expect(await result).toStrictEqual(expectedResult);
224
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
225
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(2);
226
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
227
+ ...params.bridgeTransfer,
228
+ sourceNetworkFee: networkFee,
229
+ });
230
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, expectedResult);
231
+
232
+ expect(sourceClientMock.getTransactionReceipt).toHaveBeenCalledWith({
233
+ hash: params.bridgeTransfer.sourceTxHash,
234
+ });
235
+ expect(sourceClientMock.getTransaction).toHaveBeenCalledWith({
236
+ hash: params.bridgeTransfer.sourceTxHash,
237
+ });
238
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
239
+
240
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
241
+ expect(retryPromise).toHaveBeenCalledWith({
242
+ promise: expect.any(Function),
243
+ delay,
244
+ startAfter: INITIAL_DELAY,
245
+ });
246
+ });
247
+
248
+ it('does not resolve if more confirmations are needed', async () => {
249
+ const confirmations = 3;
250
+ const targetBlockNumber = 50n;
251
+ const receiptMock = {
252
+ status: 'succeeded',
253
+ };
254
+
255
+ const params = getBridgeTrackinParams({
256
+ updateListener,
257
+ bridgeTransfer: {
258
+ requiredSourceConfirmationCount: 5,
259
+ },
260
+ });
261
+
262
+ sourceClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
263
+ sourceClientMock.getTransactionConfirmations.mockResolvedValueOnce(confirmations);
264
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
265
+
266
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
267
+ const expectedResult = {
268
+ ...params.bridgeTransfer,
269
+ sourceNetworkFee: networkFee,
270
+ sourceConfirmationCount: confirmations,
271
+ startBlockNumber: targetBlockNumber,
272
+ };
273
+
274
+ expect(await result).toBeUndefined();
275
+ expect(doneMock).not.toHaveBeenCalled();
276
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(2);
277
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
278
+ ...params.bridgeTransfer,
279
+ sourceNetworkFee: networkFee,
280
+ });
281
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, expectedResult);
282
+
283
+ expect(sourceClientMock.getTransactionReceipt).toHaveBeenCalledWith({
284
+ hash: params.bridgeTransfer.sourceTxHash,
285
+ });
286
+ expect(sourceClientMock.getTransaction).toHaveBeenCalledWith({
287
+ hash: params.bridgeTransfer.sourceTxHash,
288
+ });
289
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
290
+ expect(sourceClientMock.getTransactionConfirmations).toHaveBeenCalledWith({
291
+ hash: params.bridgeTransfer.sourceTxHash,
292
+ });
293
+
294
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
295
+ expect(retryPromise).toHaveBeenCalledWith({
296
+ promise: expect.any(Function),
297
+ delay,
298
+ startAfter: INITIAL_DELAY,
299
+ });
300
+ });
301
+
302
+ it('resolves if transaction has enough confirmations', async () => {
303
+ const nonce = 999;
304
+ const confirmations = 5;
305
+ const targetBlockNumber = 50n;
306
+ const receiptMock = {
307
+ status: 'succeeded',
308
+ logs: [{ address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING }],
309
+ };
310
+
311
+ const params = getBridgeTrackinParams({
312
+ updateListener,
313
+ bridgeTransfer: {
314
+ requiredSourceConfirmationCount: confirmations,
315
+ },
316
+ });
317
+
318
+ (decodeEventLog as jest.Mock)
319
+ .mockReturnValueOnce({ eventName: 'TransferTokens' })
320
+ .mockReturnValueOnce({ args: { nonce } });
321
+ sourceClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
322
+ sourceClientMock.getTransactionConfirmations.mockResolvedValueOnce(confirmations);
323
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
324
+
325
+ const { result } = await trackSourceTx(bridgeMock.config!, params);
326
+
327
+ const expectedResult = {
328
+ ...params.bridgeTransfer,
329
+ sourceNetworkFee: networkFee,
330
+ sourceConfirmationCount: confirmations,
331
+ targetStartedAt: now,
332
+ startBlockNumber: targetBlockNumber,
333
+ metadata: {
334
+ nonce,
335
+ },
336
+ };
337
+
338
+ expect(await result).toStrictEqual(expectedResult);
339
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
340
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(4);
341
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
342
+ ...params.bridgeTransfer,
343
+ sourceNetworkFee: networkFee,
344
+ });
345
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, {
346
+ ...params.bridgeTransfer,
347
+ sourceNetworkFee: networkFee,
348
+ sourceConfirmationCount: confirmations,
349
+ });
350
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(3, {
351
+ ...params.bridgeTransfer,
352
+ sourceNetworkFee: networkFee,
353
+ sourceConfirmationCount: confirmations,
354
+ startBlockNumber: targetBlockNumber,
355
+ });
356
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(4, expectedResult);
357
+
358
+ expect(sourceClientMock.getTransactionReceipt).toHaveBeenCalledWith({
359
+ hash: params.bridgeTransfer.sourceTxHash,
360
+ });
361
+ expect(sourceClientMock.getTransaction).toHaveBeenCalledWith({
362
+ hash: params.bridgeTransfer.sourceTxHash,
363
+ });
364
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
365
+ expect(sourceClientMock.getTransactionConfirmations).toHaveBeenCalledWith({
366
+ hash: params.bridgeTransfer.sourceTxHash,
367
+ });
368
+ expect(decodeEventLog).toHaveBeenCalledTimes(2);
369
+ expect(decodeEventLog).toHaveBeenNthCalledWith(1, {
370
+ abi: TOKEN_ROUTER_ABI,
371
+ address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
372
+ });
373
+ expect(decodeEventLog).toHaveBeenNthCalledWith(2, {
374
+ abi: TOKEN_ROUTER_ABI,
375
+ eventName: 'TransferTokens',
376
+ address: SOURCE_ROUTER_ADDRESS_RANDOMIZED_CASING,
377
+ });
378
+
379
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(SOURCE_CHAIN.chainId);
380
+ expect(retryPromise).toHaveBeenCalledWith({
381
+ promise: expect.any(Function),
382
+ delay,
383
+ startAfter: INITIAL_DELAY,
384
+ });
385
+ });
386
+ });
387
+
388
+ describe('target tx tracking', () => {
389
+ const nonce = 999;
390
+ const startBlockNumber = 12n;
391
+ const txHash = '0x124124';
392
+ const msgReceivedEvent = {
393
+ name: 'MessageReceived',
394
+ type: 'event',
395
+ inputs: [
396
+ { indexed: true, internalType: 'address', name: 'caller', type: 'address' },
397
+ { indexed: false, internalType: 'uint32', name: 'sourceDomain', type: 'uint32' },
398
+ { indexed: true, internalType: 'uint64', name: 'nonce', type: 'uint64' },
399
+ { indexed: false, internalType: 'bytes32', name: 'sender', type: 'bytes32' },
400
+ { indexed: false, internalType: 'bytes', name: 'messageBody', type: 'bytes' },
401
+ ],
402
+ };
403
+
404
+ beforeEach(() => {
405
+ (getClientForChain as jest.Mock).mockReturnValueOnce(targetClientMock);
406
+ });
407
+
408
+ it('throws if message nonce is missing', async () => {
409
+ const params = getBridgeTrackinParams({
410
+ updateListener,
411
+ });
412
+
413
+ await expect(trackTargetTx(bridgeMock.config!, params)).rejects.toThrow(
414
+ new InvalidParamsError(ErrorReason.INVALID_PARAMS, 'nonce is missing'),
415
+ );
416
+ });
417
+
418
+ it('throws if start block number is missing', async () => {
419
+ const params = getBridgeTrackinParams({
420
+ updateListener,
421
+ bridgeTransfer: {
422
+ metadata: {
423
+ nonce,
424
+ },
425
+ },
426
+ });
427
+
428
+ await expect(trackTargetTx(bridgeMock.config!, params)).rejects.toThrow(
429
+ new InvalidParamsError(ErrorReason.INVALID_PARAMS, `startBlockNumber is missing`),
430
+ );
431
+ });
432
+
433
+ it('resolves if bridging has already completed', async () => {
434
+ const params = getBridgeTrackinParams({
435
+ bridgeTransfer: {
436
+ completedAt: 1,
437
+ startBlockNumber,
438
+ metadata: {
439
+ nonce,
440
+ },
441
+ },
442
+ updateListener,
443
+ });
444
+
445
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
446
+ expect(await result).toStrictEqual(params.bridgeTransfer);
447
+ expect(doneMock).toHaveBeenCalledWith(params.bridgeTransfer);
448
+ expect(updateListenerWithoutReference).not.toHaveBeenCalled();
449
+
450
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(TARGET_CHAIN.chainId);
451
+ expect(retryPromise).toHaveBeenCalledWith({
452
+ promise: expect.any(Function),
453
+ delay,
454
+ startAfter: INITIAL_DELAY,
455
+ });
456
+ });
457
+
458
+ it('resolves if transaction has timed out', async () => {
459
+ const params = getBridgeTrackinParams({
460
+ bridgeTransfer: {
461
+ startBlockNumber,
462
+ metadata: {
463
+ nonce,
464
+ },
465
+ },
466
+ updateListener,
467
+ });
468
+
469
+ const time = params.bridgeTransfer.sourceStartedAt + TRACKING_LIMIT_MS;
470
+
471
+ jest.useFakeTimers().setSystemTime(time);
472
+
473
+ const expectedResult = {
474
+ ...params.bridgeTransfer,
475
+ completedAt: time,
476
+ errorCode: ErrorCode.TIMEOUT,
477
+ };
478
+
479
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
480
+ expect(await result).toStrictEqual(expectedResult);
481
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
482
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(1);
483
+ expect(updateListenerWithoutReference).toHaveBeenCalledWith(expectedResult);
484
+
485
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(TARGET_CHAIN.chainId);
486
+ expect(retryPromise).toHaveBeenCalledWith({
487
+ promise: expect.any(Function),
488
+ delay,
489
+ startAfter: INITIAL_DELAY,
490
+ });
491
+ });
492
+
493
+ it('resolves if startBlockNumber became invalid', async () => {
494
+ (retryPromise as jest.Mock).mockImplementationOnce(
495
+ ({ promise }: { promise: (done: () => unknown) => Promise<unknown> }) => ({
496
+ result: promise(doneMock).then(() => promise(doneMock)),
497
+ }),
498
+ );
499
+
500
+ const targetBlockNumber = 0n;
501
+
502
+ const params = getBridgeTrackinParams({
503
+ bridgeTransfer: {
504
+ requiredTargetConfirmationCount: 5,
505
+ startBlockNumber,
506
+ metadata: {
507
+ nonce,
508
+ },
509
+ },
510
+ updateListener,
511
+ });
512
+
513
+ const expectedResult = {
514
+ ...params.bridgeTransfer,
515
+ completedAt: now,
516
+ startBlockNumber: targetBlockNumber,
517
+ errorCode: ErrorCode.INVALID_PARAMS,
518
+ };
519
+
520
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
521
+ targetClientMock.getLogs.mockResolvedValueOnce([]);
522
+
523
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
524
+ expect(await result).toStrictEqual(expectedResult);
525
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
526
+
527
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(2);
528
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
529
+ ...params.bridgeTransfer,
530
+ startBlockNumber: targetBlockNumber,
531
+ });
532
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, expectedResult);
533
+
534
+ expect(targetClientMock.getTransactionReceipt).not.toHaveBeenCalled();
535
+ expect(retryPromise).toHaveBeenCalledWith({
536
+ promise: expect.any(Function),
537
+ delay,
538
+ startAfter: INITIAL_DELAY,
539
+ });
540
+ });
541
+
542
+ it('resolves if transaction was reverted', async () => {
543
+ const targetBlockNumber = 50n;
544
+ const receiptMock = {
545
+ status: 'reverted',
546
+ };
547
+ const params = getBridgeTrackinParams({
548
+ bridgeTransfer: {
549
+ startBlockNumber,
550
+ metadata: {
551
+ nonce,
552
+ },
553
+ },
554
+ updateListener,
555
+ });
556
+
557
+ targetClientMock.getLogs.mockResolvedValueOnce([{ transactionHash: txHash }]);
558
+ targetClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
559
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
560
+
561
+ const expectedResult = {
562
+ ...params.bridgeTransfer,
563
+ targetTxHash: txHash,
564
+ targetNetworkFee: networkFee,
565
+ completedAt: now,
566
+ errorCode: ErrorCode.TRANSACTION_REVERTED,
567
+ };
568
+
569
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
570
+ expect(await result).toStrictEqual(expectedResult);
571
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
572
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(3);
573
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
574
+ ...params.bridgeTransfer,
575
+ targetTxHash: txHash,
576
+ });
577
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, {
578
+ ...params.bridgeTransfer,
579
+ targetTxHash: txHash,
580
+ targetNetworkFee: networkFee,
581
+ });
582
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(3, expectedResult);
583
+
584
+ expect(targetClientMock.getLogs).toHaveBeenCalledWith({
585
+ address: TARGET_TRANSMITTER_ADDRESS,
586
+ event: msgReceivedEvent,
587
+ args: { nonce },
588
+ fromBlock: 'earliest',
589
+ toBlock: 'latest',
590
+ });
591
+ expect(targetClientMock.getTransactionReceipt).toHaveBeenCalledWith({ hash: txHash });
592
+ expect(targetClientMock.getTransaction).toHaveBeenCalledWith({ hash: txHash });
593
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
594
+
595
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(TARGET_CHAIN.chainId);
596
+ expect(retryPromise).toHaveBeenCalledWith({
597
+ promise: expect.any(Function),
598
+ delay,
599
+ startAfter: INITIAL_DELAY,
600
+ });
601
+ });
602
+
603
+ it('does not resolve if transaction hash is unknown', async () => {
604
+ const targetBlockNumber = 50;
605
+
606
+ const params = getBridgeTrackinParams({
607
+ bridgeTransfer: {
608
+ requiredTargetConfirmationCount: 5,
609
+ startBlockNumber,
610
+ metadata: {
611
+ nonce,
612
+ },
613
+ },
614
+ updateListener,
615
+ });
616
+
617
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
618
+ targetClientMock.getLogs.mockResolvedValueOnce([]);
619
+
620
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
621
+ expect(await result).toBeUndefined();
622
+ expect(doneMock).not.toHaveBeenCalled();
623
+
624
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(1);
625
+ expect(updateListenerWithoutReference).toHaveBeenCalledWith({
626
+ ...params.bridgeTransfer,
627
+ startBlockNumber: targetBlockNumber,
628
+ });
629
+
630
+ expect(targetClientMock.getLogs).toHaveBeenCalledWith({
631
+ address: TARGET_TRANSMITTER_ADDRESS,
632
+ event: msgReceivedEvent,
633
+ args: { nonce },
634
+ fromBlock: 'earliest',
635
+ toBlock: 'latest',
636
+ });
637
+
638
+ expect(targetClientMock.getTransactionReceipt).not.toHaveBeenCalled();
639
+ expect(retryPromise).toHaveBeenCalledWith({
640
+ promise: expect.any(Function),
641
+ delay,
642
+ startAfter: INITIAL_DELAY,
643
+ });
644
+ });
645
+
646
+ it('does not resolve if more confirmations are needed', async () => {
647
+ const targetBlockNumber = 50;
648
+ const receiptMock = {
649
+ status: 'succeeded',
650
+ };
651
+ const confirmations = 3;
652
+ const params = getBridgeTrackinParams({
653
+ bridgeTransfer: {
654
+ requiredTargetConfirmationCount: 5,
655
+ startBlockNumber,
656
+ metadata: {
657
+ nonce,
658
+ },
659
+ },
660
+ updateListener,
661
+ });
662
+
663
+ targetClientMock.getLogs.mockResolvedValueOnce([{ transactionHash: txHash }]);
664
+ targetClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
665
+ targetClientMock.getTransactionConfirmations.mockResolvedValueOnce(confirmations);
666
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
667
+
668
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
669
+ expect(await result).toBeUndefined();
670
+ expect(doneMock).not.toHaveBeenCalled();
671
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(3);
672
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
673
+ ...params.bridgeTransfer,
674
+ targetTxHash: txHash,
675
+ });
676
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, {
677
+ ...params.bridgeTransfer,
678
+ targetTxHash: txHash,
679
+ targetNetworkFee: networkFee,
680
+ });
681
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(3, {
682
+ ...params.bridgeTransfer,
683
+ targetTxHash: txHash,
684
+ targetNetworkFee: networkFee,
685
+ targetConfirmationCount: confirmations,
686
+ });
687
+
688
+ expect(targetClientMock.getLogs).toHaveBeenCalledWith({
689
+ address: TARGET_TRANSMITTER_ADDRESS,
690
+ event: msgReceivedEvent,
691
+ args: { nonce },
692
+ fromBlock: 'earliest',
693
+ toBlock: 'latest',
694
+ });
695
+ expect(targetClientMock.getTransactionReceipt).toHaveBeenCalledWith({ hash: txHash });
696
+ expect(targetClientMock.getTransaction).toHaveBeenCalledWith({ hash: txHash });
697
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
698
+ expect(targetClientMock.getTransactionConfirmations).toHaveBeenCalledWith({ hash: txHash });
699
+
700
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(TARGET_CHAIN.chainId);
701
+ expect(retryPromise).toHaveBeenCalledWith({
702
+ promise: expect.any(Function),
703
+ delay,
704
+ startAfter: INITIAL_DELAY,
705
+ });
706
+ });
707
+
708
+ it('resolves if transaction has enough confirmations', async () => {
709
+ const targetBlockNumber = MAX_BLOCKS * 6n;
710
+ const receiptMock = {
711
+ status: 'succeeded',
712
+ };
713
+ const confirmations = 3;
714
+ const params = getBridgeTrackinParams({
715
+ bridgeTransfer: {
716
+ requiredTargetConfirmationCount: confirmations,
717
+ startBlockNumber: MAX_BLOCKS * 2n,
718
+ metadata: {
719
+ nonce,
720
+ },
721
+ },
722
+ updateListener,
723
+ });
724
+
725
+ const expectedResult = {
726
+ ...params.bridgeTransfer,
727
+ targetTxHash: txHash,
728
+ targetNetworkFee: networkFee,
729
+ targetConfirmationCount: confirmations,
730
+ completedAt: now,
731
+ };
732
+
733
+ targetClientMock.getLogs.mockResolvedValueOnce([{ transactionHash: txHash }]);
734
+ targetClientMock.getTransactionReceipt.mockResolvedValueOnce(receiptMock);
735
+ targetClientMock.getTransactionConfirmations.mockResolvedValueOnce(confirmations);
736
+ targetClientMock.getBlockNumber.mockResolvedValueOnce(targetBlockNumber);
737
+
738
+ const { result } = await trackTargetTx(bridgeMock.config!, params);
739
+ expect(await result).toStrictEqual(expectedResult);
740
+ expect(doneMock).toHaveBeenCalledWith(expectedResult);
741
+ expect(updateListenerWithoutReference).toHaveBeenCalledTimes(4);
742
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(1, {
743
+ ...params.bridgeTransfer,
744
+ targetTxHash: txHash,
745
+ });
746
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(2, {
747
+ ...params.bridgeTransfer,
748
+ targetTxHash: txHash,
749
+ targetNetworkFee: networkFee,
750
+ });
751
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(3, {
752
+ ...params.bridgeTransfer,
753
+ targetTxHash: txHash,
754
+ targetNetworkFee: networkFee,
755
+ targetConfirmationCount: confirmations,
756
+ });
757
+ expect(updateListenerWithoutReference).toHaveBeenNthCalledWith(4, expectedResult);
758
+
759
+ expect(targetClientMock.getLogs).toHaveBeenCalledWith({
760
+ address: TARGET_TRANSMITTER_ADDRESS,
761
+ event: msgReceivedEvent,
762
+ args: { nonce },
763
+ fromBlock: params.bridgeTransfer.startBlockNumber! - MAX_BLOCKS,
764
+ toBlock: params.bridgeTransfer.startBlockNumber! + MAX_BLOCKS,
765
+ });
766
+ expect(targetClientMock.getTransactionReceipt).toHaveBeenCalledWith({ hash: txHash });
767
+ expect(targetClientMock.getTransaction).toHaveBeenCalledWith({ hash: txHash });
768
+ expect(getNetworkFeeEVM).toHaveBeenCalledWith(txMock, receiptMock);
769
+ expect(targetClientMock.getTransactionConfirmations).toHaveBeenCalledWith({ hash: txHash });
770
+
771
+ expect(getTrackingDelayByChainId).toHaveBeenCalledWith(TARGET_CHAIN.chainId);
772
+ expect(retryPromise).toHaveBeenCalledWith({
773
+ promise: expect.any(Function),
774
+ delay,
775
+ startAfter: INITIAL_DELAY,
776
+ });
777
+ });
778
+ });
779
+ });