@gibs/bridge-sdk 1.10.1 → 1.12.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.
@@ -0,0 +1,968 @@
1
+ import type { Hex } from 'viem';
2
+ import { zeroAddress } from 'viem';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import {
5
+ chainMulticall,
6
+ fetchPriceCorrective,
7
+ links,
8
+ loadBridgeFees,
9
+ minBridgeAmountIn,
10
+ nativeSymbol,
11
+ readAmountOut,
12
+ tokenBridgeInfo,
13
+ } from './chain-info';
14
+ import { Chains, nativeAssetOut, Providers, pathway } from './config';
15
+
16
+ // Mock viem and multicall
17
+ vi.mock('viem', async () => {
18
+ const actual = await vi.importActual<typeof import('viem')>('viem');
19
+ return {
20
+ ...actual,
21
+ getContract: vi.fn(() => ({
22
+ read: {
23
+ mediatorContractOnOtherSide: vi.fn(),
24
+ getBridgeMode: vi.fn(),
25
+ minPerTx: vi.fn(),
26
+ getFee: vi.fn(),
27
+ FOREIGN_TO_HOME_FEE: vi.fn(),
28
+ HOME_TO_FOREIGN_FEE: vi.fn(),
29
+ calculateFee: vi.fn(),
30
+ getAmountToBurn: vi.fn(),
31
+ priceCorrective: vi.fn(),
32
+ },
33
+ })),
34
+ };
35
+ });
36
+
37
+ vi.mock('@gibs/common/multicall', () => ({
38
+ multicallRead: vi.fn(),
39
+ }));
40
+
41
+ describe('chain-info', () => {
42
+ describe('nativeSymbol', () => {
43
+ it('should return empty string for null asset', () => {
44
+ expect(nativeSymbol(null)).toBe('');
45
+ });
46
+
47
+ it('should return symbol when not unwrapping', () => {
48
+ const asset = {
49
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
50
+ symbol: 'USDC',
51
+ chainId: 1,
52
+ };
53
+ expect(nativeSymbol(asset, false)).toBe('USDC');
54
+ });
55
+
56
+ it('should return symbol without first character when unwrapping native asset', () => {
57
+ const wethAddress = nativeAssetOut[Chains.ETH];
58
+ const asset = {
59
+ address: wethAddress,
60
+ symbol: 'WETH',
61
+ chainId: 1,
62
+ };
63
+ expect(nativeSymbol(asset, true)).toBe('ETH');
64
+ });
65
+
66
+ it('should not strip character if unwrap is true but not a native asset', () => {
67
+ const asset = {
68
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
69
+ symbol: 'USDC',
70
+ chainId: 1,
71
+ };
72
+ expect(nativeSymbol(asset, true)).toBe('USDC');
73
+ });
74
+
75
+ it('should handle WPLS unwrapping', () => {
76
+ const wplsAddress = nativeAssetOut[Chains.PLS];
77
+ const asset = {
78
+ address: wplsAddress,
79
+ symbol: 'WPLS',
80
+ chainId: parseInt(Chains.PLS, 16),
81
+ };
82
+ expect(nativeSymbol(asset, true)).toBe('PLS');
83
+ });
84
+
85
+ it('should handle WBNB unwrapping', () => {
86
+ const wbnbAddress = nativeAssetOut[Chains.BNB];
87
+ const asset = {
88
+ address: wbnbAddress,
89
+ symbol: 'WBNB',
90
+ chainId: parseInt(Chains.BNB, 16),
91
+ };
92
+ expect(nativeSymbol(asset, true)).toBe('BNB');
93
+ });
94
+
95
+ it('should return full symbol when unwrap is false', () => {
96
+ const wethAddress = nativeAssetOut[Chains.ETH];
97
+ const asset = {
98
+ address: wethAddress,
99
+ symbol: 'WETH',
100
+ chainId: 1,
101
+ };
102
+ expect(nativeSymbol(asset, false)).toBe('WETH');
103
+ });
104
+
105
+ it('should handle empty symbol', () => {
106
+ const asset = {
107
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
108
+ symbol: '',
109
+ chainId: 1,
110
+ };
111
+ expect(nativeSymbol(asset, false)).toBe('');
112
+ });
113
+
114
+ it('should default unwrap to false when not provided', () => {
115
+ const asset = {
116
+ address: nativeAssetOut[Chains.ETH],
117
+ symbol: 'WETH',
118
+ chainId: 1,
119
+ };
120
+ // Without unwrap parameter, should return full symbol
121
+ expect(nativeSymbol(asset)).toBe('WETH');
122
+ });
123
+ });
124
+
125
+ describe('links', () => {
126
+ let mockClient: any;
127
+
128
+ beforeEach(() => {
129
+ mockClient = { chain: { id: 1 } };
130
+ vi.clearAllMocks();
131
+ });
132
+
133
+ it('should fetch link info for a token', async () => {
134
+ const { multicallRead } = await import('@gibs/common/multicall');
135
+ (multicallRead as any).mockResolvedValue(['0xBridgedToken' as Hex, '0xNativeToken' as Hex]);
136
+
137
+ const result = await links({
138
+ chainId: 1,
139
+ target: '0xBridgeAddress' as Hex,
140
+ address: '0xTokenAddress' as Hex,
141
+ client: mockClient,
142
+ });
143
+
144
+ expect(result).toBeDefined();
145
+ expect(Array.isArray(result)).toBe(true);
146
+ expect(result[0]).toBe('0xBridgedToken');
147
+ expect(result[1]).toBe('0xNativeToken');
148
+ expect(multicallRead).toHaveBeenCalled();
149
+ });
150
+ });
151
+
152
+ describe('tokenBridgeInfo', () => {
153
+ let mockFromClient: any;
154
+ let mockToClient: any;
155
+
156
+ beforeEach(() => {
157
+ mockFromClient = { chain: { id: 1 } };
158
+ mockToClient = { chain: { id: 369 } };
159
+ vi.clearAllMocks();
160
+ // Clear memoization cache for links function
161
+ (links as any).cache = new Map();
162
+ });
163
+
164
+ it('should return null when assetIn is null', async () => {
165
+ const result = await tokenBridgeInfo({
166
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
167
+ assetIn: null,
168
+ isProd: true,
169
+ fromChainClient: mockFromClient,
170
+ toChainClient: mockToClient,
171
+ });
172
+ expect(result).toBeNull();
173
+ });
174
+
175
+ it('should fetch token bridge info successfully', async () => {
176
+ const { multicallRead } = await import('@gibs/common/multicall');
177
+ (multicallRead as any).mockResolvedValue([['0xForeignToken' as Hex, '0xNativeToken' as Hex]]);
178
+
179
+ const assetIn = {
180
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
181
+ symbol: 'USDC',
182
+ decimals: 6,
183
+ name: 'USD Coin',
184
+ chainId: 1,
185
+ logoURI: null,
186
+ };
187
+
188
+ const result = await tokenBridgeInfo({
189
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
190
+ assetIn,
191
+ isProd: true,
192
+ fromChainClient: mockFromClient,
193
+ toChainClient: mockToClient,
194
+ });
195
+
196
+ expect(result).toBeDefined();
197
+ expect(result?.assetInAddress).toBeDefined();
198
+ });
199
+
200
+ it('should handle toBridged path when toBridged is not zero', async () => {
201
+ const { multicallRead } = await import('@gibs/common/multicall');
202
+ const toBridgedAddress = '0x1234567890123456789012345678901234567890' as Hex;
203
+ (multicallRead as any).mockReset();
204
+ (multicallRead as any)
205
+ .mockResolvedValueOnce([toBridgedAddress, zeroAddress]) // First links() call - toMappings
206
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]); // Second links() call - fromMappings
207
+
208
+ const assetIn = {
209
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
210
+ symbol: 'USDC',
211
+ decimals: 6,
212
+ name: 'USD Coin',
213
+ chainId: 1,
214
+ logoURI: null,
215
+ };
216
+
217
+ const result = await tokenBridgeInfo({
218
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
219
+ assetIn,
220
+ isProd: true,
221
+ fromChainClient: mockFromClient,
222
+ toChainClient: mockToClient,
223
+ });
224
+
225
+ expect(result).toBeDefined();
226
+ expect(result?.assetOutAddress).toBe(toBridgedAddress);
227
+ expect(result?.originationChainId).toBe(Chains.PLS);
228
+ expect(result?.toHome).toBeDefined();
229
+ expect(result?.toHome?.home).toBe(toBridgedAddress);
230
+ });
231
+
232
+ it('should handle toNative path when toNative is not zero', async () => {
233
+ const { multicallRead } = await import('@gibs/common/multicall');
234
+ const toNativeAddress = '0x2345678901234567890123456789012345678901' as Hex;
235
+ (multicallRead as any).mockReset();
236
+ (multicallRead as any)
237
+ .mockResolvedValueOnce([zeroAddress, toNativeAddress]) // First links() call - toMappings
238
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]); // Second links() call - fromMappings
239
+
240
+ const assetIn = {
241
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
242
+ symbol: 'USDC',
243
+ decimals: 6,
244
+ name: 'USD Coin',
245
+ chainId: 1,
246
+ logoURI: null,
247
+ };
248
+
249
+ const result = await tokenBridgeInfo({
250
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
251
+ assetIn,
252
+ isProd: true,
253
+ fromChainClient: mockFromClient,
254
+ toChainClient: mockToClient,
255
+ });
256
+
257
+ expect(result).toBeDefined();
258
+ expect(result?.assetOutAddress).toBe(toNativeAddress);
259
+ expect(result?.originationChainId).toBe(Chains.ETH);
260
+ expect(result?.toForeign).toBeDefined();
261
+ expect(result?.toForeign?.home).toBe(toNativeAddress);
262
+ });
263
+
264
+ it('should handle fromNative path when fromNative is not zero', async () => {
265
+ const { multicallRead } = await import('@gibs/common/multicall');
266
+ const fromNativeAddress = '0x3456789012345678901234567890123456789012' as Hex;
267
+ (multicallRead as any).mockReset();
268
+ (multicallRead as any)
269
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]) // First links() call - toMappings
270
+ .mockResolvedValueOnce([zeroAddress, fromNativeAddress]); // Second links() call - fromMappings
271
+
272
+ const assetIn = {
273
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
274
+ symbol: 'USDC',
275
+ decimals: 6,
276
+ name: 'USD Coin',
277
+ chainId: 1,
278
+ logoURI: null,
279
+ };
280
+
281
+ const result = await tokenBridgeInfo({
282
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
283
+ assetIn,
284
+ isProd: true,
285
+ fromChainClient: mockFromClient,
286
+ toChainClient: mockToClient,
287
+ });
288
+
289
+ expect(result).toBeDefined();
290
+ expect(result?.assetOutAddress).toBe(fromNativeAddress);
291
+ expect(result?.originationChainId).toBe(Chains.ETH);
292
+ expect(result?.toHome).toBeDefined();
293
+ });
294
+
295
+ it('should handle fromBridged path when fromBridged is not zero', async () => {
296
+ const { multicallRead } = await import('@gibs/common/multicall');
297
+ const fromBridgedAddress = '0x4567890123456789012345678901234567890123' as Hex;
298
+ (multicallRead as any).mockReset();
299
+ (multicallRead as any)
300
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]) // First links() call - toMappings
301
+ .mockResolvedValueOnce([fromBridgedAddress, zeroAddress]); // Second links() call - fromMappings
302
+
303
+ const assetIn = {
304
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
305
+ symbol: 'USDC',
306
+ decimals: 6,
307
+ name: 'USD Coin',
308
+ chainId: 1,
309
+ logoURI: null,
310
+ };
311
+
312
+ const result = await tokenBridgeInfo({
313
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
314
+ assetIn,
315
+ isProd: true,
316
+ fromChainClient: mockFromClient,
317
+ toChainClient: mockToClient,
318
+ });
319
+
320
+ expect(result).toBeDefined();
321
+ expect(result?.assetOutAddress).toBe(fromBridgedAddress);
322
+ expect(result?.originationChainId).toBe(Chains.PLS);
323
+ expect(result?.toForeign).toBeDefined();
324
+ });
325
+
326
+ it('should handle not-yet-bridged token with toHome pathway', async () => {
327
+ const { multicallRead } = await import('@gibs/common/multicall');
328
+ (multicallRead as any).mockReset();
329
+ (multicallRead as any)
330
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]) // First links() call - toMappings
331
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]); // Second links() call - fromMappings
332
+
333
+ const assetIn = {
334
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
335
+ symbol: 'USDC',
336
+ decimals: 6,
337
+ name: 'USD Coin',
338
+ chainId: 1,
339
+ logoURI: null,
340
+ };
341
+
342
+ // Create pathway with toHome = true
343
+ const bridgePathway = pathway([Providers.PULSECHAIN, Chains.ETH, Chains.PLS], true);
344
+ expect(bridgePathway?.toHome).toBe(true);
345
+
346
+ const result = await tokenBridgeInfo({
347
+ bridgeKey: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
348
+ assetIn,
349
+ isProd: true,
350
+ fromChainClient: mockFromClient,
351
+ toChainClient: mockToClient,
352
+ });
353
+
354
+ expect(result).toBeDefined();
355
+ expect(result?.assetOutAddress).toBeNull();
356
+ expect(result?.toHome).toBeDefined();
357
+ expect(result?.toHome?.home).toBeNull();
358
+ });
359
+
360
+ it('should handle not-yet-bridged token with toForeign pathway', async () => {
361
+ const { multicallRead } = await import('@gibs/common/multicall');
362
+ (multicallRead as any).mockReset();
363
+ (multicallRead as any)
364
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]) // First links() call - toMappings
365
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]); // Second links() call - fromMappings
366
+
367
+ const assetIn = {
368
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
369
+ symbol: 'USDC',
370
+ decimals: 6,
371
+ name: 'USD Coin',
372
+ chainId: 1,
373
+ logoURI: null,
374
+ };
375
+
376
+ // Create pathway with toHome = false (toForeign)
377
+ const bridgePathway = pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true);
378
+ expect(bridgePathway?.toHome).toBe(false);
379
+
380
+ const result = await tokenBridgeInfo({
381
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
382
+ assetIn,
383
+ isProd: true,
384
+ fromChainClient: mockFromClient,
385
+ toChainClient: mockToClient,
386
+ });
387
+
388
+ expect(result).toBeDefined();
389
+ expect(result?.assetOutAddress).toBeNull();
390
+ expect(result?.toForeign).toBeDefined();
391
+ expect(result?.toForeign?.foreign).toBeNull();
392
+ });
393
+
394
+ it('should wrap native asset address to nativeAssetOut', async () => {
395
+ const { multicallRead } = await import('@gibs/common/multicall');
396
+ const toBridgedAddress = '0x1234567890123456789012345678901234567890' as Hex;
397
+ (multicallRead as any).mockReset();
398
+ (multicallRead as any)
399
+ .mockResolvedValueOnce([toBridgedAddress, zeroAddress]) // First links() call - toMappings
400
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]); // Second links() call - fromMappings
401
+
402
+ const assetIn = {
403
+ address: zeroAddress,
404
+ symbol: 'PLS',
405
+ decimals: 18,
406
+ name: 'Pulse',
407
+ chainId: 369,
408
+ logoURI: null,
409
+ };
410
+
411
+ await tokenBridgeInfo({
412
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
413
+ assetIn,
414
+ isProd: true,
415
+ fromChainClient: mockFromClient,
416
+ toChainClient: mockToClient,
417
+ });
418
+
419
+ // Verify that links was called with nativeAssetOut[fromChain] instead of zeroAddress
420
+ expect(multicallRead).toHaveBeenCalled();
421
+ const callArgs = (multicallRead as any).mock.calls[0][0];
422
+ // The target should be one of the bridge addresses, not checking the exact value here
423
+ expect(callArgs).toBeDefined();
424
+ });
425
+ });
426
+
427
+ describe('chainMulticall', () => {
428
+ it('should create chainMulticall helper', () => {
429
+ const mockClient = { chain: { id: 1 } };
430
+ const result = chainMulticall(mockClient as any);
431
+
432
+ expect(result).toBeDefined();
433
+ expect(typeof result).toBe('object');
434
+ });
435
+ });
436
+
437
+ describe('loadBridgeFees', () => {
438
+ let mockFromClient: any;
439
+ let mockToClient: any;
440
+
441
+ beforeEach(() => {
442
+ vi.clearAllMocks();
443
+
444
+ mockFromClient = {
445
+ chain: {
446
+ id: 1,
447
+ contracts: {
448
+ multicall3: { address: '0xca11bde05977b3631167028862be2a173976ca11' as Hex },
449
+ },
450
+ },
451
+ };
452
+
453
+ mockToClient = {
454
+ chain: {
455
+ id: 369,
456
+ contracts: {
457
+ multicall3: { address: '0xca11bde05977b3631167028862be2a173976ca11' as Hex },
458
+ },
459
+ },
460
+ };
461
+ });
462
+
463
+ it('should load bridge fees from "from" side fee manager', async () => {
464
+ const { getContract } = await import('viem');
465
+
466
+ // Mock the sequential aggregate3 calls
467
+ let callCount = 0;
468
+ const mockAggregate3 = vi.fn().mockImplementation(() => {
469
+ callCount++;
470
+ if (callCount === 1) {
471
+ // First call: get feeManager address
472
+ return Promise.resolve([
473
+ {
474
+ success: true,
475
+ returnData: '0x000000000000000000000000feeManager1234567890123456789012345678' as Hex,
476
+ },
477
+ ]);
478
+ } else if (callCount === 2) {
479
+ // Second call: get fee keys (HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE) - must be bytes32
480
+ return Promise.resolve([
481
+ {
482
+ success: true,
483
+ returnData:
484
+ '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
485
+ },
486
+ {
487
+ success: true,
488
+ returnData:
489
+ '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex,
490
+ },
491
+ ]);
492
+ } else {
493
+ // Third call: get actual fees
494
+ return Promise.resolve([
495
+ {
496
+ success: true,
497
+ returnData:
498
+ '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex,
499
+ },
500
+ {
501
+ success: true,
502
+ returnData:
503
+ '0x0000000000000000000000000000000000000000000000001bc16d674ec80000' as Hex,
504
+ },
505
+ ]);
506
+ }
507
+ });
508
+
509
+ (getContract as any).mockReturnValue({
510
+ read: {
511
+ aggregate3: mockAggregate3,
512
+ },
513
+ });
514
+
515
+ const bridgePathway = {
516
+ ...pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true)!,
517
+ feeManager: 'from' as const,
518
+ };
519
+
520
+ const result = await loadBridgeFees({
521
+ pathway: bridgePathway,
522
+ fromChainClient: mockFromClient,
523
+ toChainClient: mockToClient,
524
+ });
525
+
526
+ expect(result).toBeDefined();
527
+ expect(result.feeManager).toBeDefined();
528
+ expect(result.feeH2F).toBeDefined();
529
+ expect(result.feeF2H).toBeDefined();
530
+ expect(mockAggregate3).toHaveBeenCalledTimes(3);
531
+ });
532
+
533
+ it('should load bridge fees from "to" side fee manager', async () => {
534
+ const { getContract } = await import('viem');
535
+
536
+ let callCount = 0;
537
+ const mockAggregate3 = vi.fn().mockImplementation(() => {
538
+ callCount++;
539
+ if (callCount === 1) {
540
+ return Promise.resolve([
541
+ {
542
+ success: true,
543
+ returnData: '0x000000000000000000000000feeManager9876543210987654321098765432' as Hex,
544
+ },
545
+ ]);
546
+ } else if (callCount === 2) {
547
+ return Promise.resolve([
548
+ {
549
+ success: true,
550
+ returnData:
551
+ '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
552
+ },
553
+ {
554
+ success: true,
555
+ returnData:
556
+ '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex,
557
+ },
558
+ ]);
559
+ } else {
560
+ return Promise.resolve([
561
+ {
562
+ success: true,
563
+ returnData:
564
+ '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex,
565
+ },
566
+ {
567
+ success: true,
568
+ returnData:
569
+ '0x0000000000000000000000000000000000000000000000001bc16d674ec80000' as Hex,
570
+ },
571
+ ]);
572
+ }
573
+ });
574
+
575
+ (getContract as any).mockReturnValue({
576
+ read: {
577
+ aggregate3: mockAggregate3,
578
+ },
579
+ });
580
+
581
+ const bridgePathway = {
582
+ ...pathway([Providers.PULSECHAIN, Chains.ETH, Chains.PLS], true)!,
583
+ feeManager: 'to' as const,
584
+ };
585
+
586
+ const result = await loadBridgeFees({
587
+ pathway: bridgePathway,
588
+ fromChainClient: mockFromClient,
589
+ toChainClient: mockToClient,
590
+ });
591
+
592
+ expect(result).toBeDefined();
593
+ expect(mockAggregate3).toHaveBeenCalledTimes(3);
594
+ });
595
+
596
+ it('should throw error when feeManager call fails', async () => {
597
+ const { getContract } = await import('viem');
598
+
599
+ const mockAggregate3 = vi.fn().mockResolvedValue([
600
+ {
601
+ success: false,
602
+ returnData: '0x' as Hex,
603
+ },
604
+ ]);
605
+
606
+ (getContract as any).mockReturnValue({
607
+ read: {
608
+ aggregate3: mockAggregate3,
609
+ },
610
+ });
611
+
612
+ const bridgePathway = pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true)!;
613
+
614
+ await expect(
615
+ loadBridgeFees({
616
+ pathway: bridgePathway,
617
+ fromChainClient: mockFromClient,
618
+ toChainClient: mockToClient,
619
+ }),
620
+ ).rejects.toThrow('unable to load feeManager');
621
+ });
622
+
623
+ it('should throw error when feeManager returns empty data', async () => {
624
+ const { getContract } = await import('viem');
625
+
626
+ const mockAggregate3 = vi.fn().mockResolvedValue([
627
+ {
628
+ success: true,
629
+ returnData: '0x' as Hex,
630
+ },
631
+ ]);
632
+
633
+ (getContract as any).mockReturnValue({
634
+ read: {
635
+ aggregate3: mockAggregate3,
636
+ },
637
+ });
638
+
639
+ const bridgePathway = pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true)!;
640
+
641
+ await expect(
642
+ loadBridgeFees({
643
+ pathway: bridgePathway,
644
+ fromChainClient: mockFromClient,
645
+ toChainClient: mockToClient,
646
+ }),
647
+ ).rejects.toThrow('unable to read feeManager');
648
+ });
649
+
650
+ it('should handle feeManager address without zero padding (line 414 coverage)', async () => {
651
+ const { getContract } = await import('viem');
652
+
653
+ let callCount = 0;
654
+ const mockAggregate3 = vi.fn().mockImplementation(() => {
655
+ callCount++;
656
+ if (callCount === 1) {
657
+ // Return feeManager address WITHOUT zero padding (triggers line 414)
658
+ return Promise.resolve([
659
+ {
660
+ success: true,
661
+ returnData: '0x1234567890123456789012345678901234567890ABCDEFABCDEFABCDEF' as Hex,
662
+ },
663
+ ]);
664
+ } else if (callCount === 2) {
665
+ return Promise.resolve([
666
+ {
667
+ success: true,
668
+ returnData:
669
+ '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
670
+ },
671
+ {
672
+ success: true,
673
+ returnData:
674
+ '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex,
675
+ },
676
+ ]);
677
+ } else {
678
+ return Promise.resolve([
679
+ {
680
+ success: true,
681
+ returnData:
682
+ '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex,
683
+ },
684
+ {
685
+ success: true,
686
+ returnData:
687
+ '0x0000000000000000000000000000000000000000000000001bc16d674ec80000' as Hex,
688
+ },
689
+ ]);
690
+ }
691
+ });
692
+
693
+ (getContract as any).mockReturnValue({
694
+ read: {
695
+ aggregate3: mockAggregate3,
696
+ },
697
+ });
698
+
699
+ const bridgePathway = {
700
+ ...pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true)!,
701
+ feeManager: 'from' as const,
702
+ };
703
+
704
+ const result = await loadBridgeFees({
705
+ pathway: bridgePathway,
706
+ fromChainClient: mockFromClient,
707
+ toChainClient: mockToClient,
708
+ });
709
+
710
+ expect(result).toBeDefined();
711
+ expect(result.feeManager).toBeDefined();
712
+ // Verify the address was extracted from the last 40 characters (slice(-40))
713
+ expect(result.feeManager.toLowerCase()).toBe('0x9012345678901234567890abcdefabcdefabcdef');
714
+ });
715
+ });
716
+
717
+ describe('minBridgeAmountIn', () => {
718
+ let mockFromClient: any;
719
+ let mockToClient: any;
720
+ let mockAssetIn: any;
721
+
722
+ beforeEach(() => {
723
+ mockFromClient = {
724
+ chain: { id: 1 },
725
+ readContract: vi.fn(),
726
+ };
727
+ mockToClient = {
728
+ chain: { id: 369 },
729
+ readContract: vi.fn(),
730
+ };
731
+ mockAssetIn = {
732
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
733
+ symbol: 'USDC',
734
+ decimals: 6,
735
+ chainId: 1,
736
+ };
737
+ vi.clearAllMocks();
738
+ });
739
+
740
+ it('should fetch min amount from "from" side', async () => {
741
+ const bridgePathway = {
742
+ ...pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true)!,
743
+ feeManager: 'from' as const,
744
+ };
745
+
746
+ (mockFromClient.readContract as any).mockResolvedValue(1000000000000000n);
747
+
748
+ const result = await minBridgeAmountIn({
749
+ assetIn: mockAssetIn,
750
+ pathway: bridgePathway,
751
+ fromPublicClient: mockFromClient,
752
+ toPublicClient: mockToClient,
753
+ });
754
+
755
+ expect(result).toBe(1000000000000000n);
756
+ expect(mockFromClient.readContract).toHaveBeenCalled();
757
+ });
758
+
759
+ it('should fetch min amount from "to" side', async () => {
760
+ const bridgePathway = {
761
+ ...pathway([Providers.PULSECHAIN, Chains.ETH, Chains.PLS], true)!,
762
+ feeManager: 'to' as const,
763
+ };
764
+
765
+ (mockToClient.readContract as any).mockResolvedValue(2000000000000000n);
766
+
767
+ const result = await minBridgeAmountIn({
768
+ assetIn: mockAssetIn,
769
+ pathway: bridgePathway,
770
+ fromPublicClient: mockFromClient,
771
+ toPublicClient: mockToClient,
772
+ });
773
+
774
+ expect(result).toBe(2000000000000000n);
775
+ expect(mockToClient.readContract).toHaveBeenCalled();
776
+ });
777
+ });
778
+
779
+ describe('fetchPriceCorrective', () => {
780
+ let mockFromClient: any;
781
+ let mockToClient: any;
782
+ let mockAssetOut: any;
783
+ let mockAssetLink: any;
784
+
785
+ beforeEach(() => {
786
+ mockFromClient = { chain: { id: 1 } };
787
+ mockToClient = { chain: { id: 369 } };
788
+ mockAssetOut = {
789
+ address: '0xAssetOut123456789012345678901234567890' as Hex,
790
+ symbol: 'OUT',
791
+ decimals: 18,
792
+ name: 'Asset Out',
793
+ chainId: 1,
794
+ logoURI: null,
795
+ };
796
+ mockAssetLink = {
797
+ assetInAddress: '0xAssetIn1234567890123456789012345678901' as Hex,
798
+ assetOutAddress: '0xAssetOut123456789012345678901234567890' as Hex,
799
+ originationChainId: 1,
800
+ };
801
+ vi.clearAllMocks();
802
+ });
803
+
804
+ it('should return helper object with three functions', () => {
805
+ const result = fetchPriceCorrective({
806
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
807
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
808
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
809
+ assetOut: mockAssetOut,
810
+ fromChainClient: mockFromClient,
811
+ toChainClient: mockToClient,
812
+ assetLink: mockAssetLink,
813
+ amountIn: 1000000000000000000n,
814
+ isProd: true,
815
+ });
816
+
817
+ expect(result).toBeDefined();
818
+ expect(result.getPaymentToken).toBeInstanceOf(Function);
819
+ expect(result.getSwapResults).toBeInstanceOf(Function);
820
+ expect(result.reduceResults).toBeInstanceOf(Function);
821
+ });
822
+
823
+ it('should call tokenBridgeInfo in getPaymentToken', async () => {
824
+ const { multicallRead } = await import('@gibs/common/multicall');
825
+ (multicallRead as any).mockReset();
826
+ (multicallRead as any)
827
+ .mockResolvedValueOnce(['0x1234567890123456789012345678901234567890' as Hex, zeroAddress])
828
+ .mockResolvedValueOnce([zeroAddress, zeroAddress]);
829
+ (links as any).cache = new Map();
830
+
831
+ const helpers = fetchPriceCorrective({
832
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
833
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
834
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
835
+ assetOut: mockAssetOut,
836
+ fromChainClient: mockFromClient,
837
+ toChainClient: mockToClient,
838
+ assetLink: mockAssetLink,
839
+ amountIn: 1000000000000000000n,
840
+ isProd: true,
841
+ });
842
+
843
+ const paymentToken = await helpers.getPaymentToken();
844
+
845
+ expect(paymentToken).toBeDefined();
846
+ expect(multicallRead).toHaveBeenCalled();
847
+ });
848
+
849
+ it('should call readAmountOut twice in getSwapResults', async () => {
850
+ const { multicallRead } = await import('@gibs/common/multicall');
851
+ (multicallRead as any).mockReset();
852
+ (multicallRead as any).mockResolvedValue([[1000000000000000000n, 950000000000000000n]]);
853
+
854
+ const helpers = fetchPriceCorrective({
855
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
856
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
857
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
858
+ assetOut: mockAssetOut,
859
+ fromChainClient: mockFromClient,
860
+ toChainClient: mockToClient,
861
+ assetLink: mockAssetLink,
862
+ amountIn: 1000000000000000000n,
863
+ isProd: true,
864
+ });
865
+
866
+ const mockPaymentTokenResult = {
867
+ assetInAddress: '0xAssetIn1234567890123456789012345678901' as Hex,
868
+ assetOutAddress: '0xPaymentOut12345678901234567890123456789' as Hex,
869
+ originationChainId: 1,
870
+ };
871
+
872
+ const results = await helpers.getSwapResults({
873
+ paymentTokenResult: mockPaymentTokenResult,
874
+ chain: Chains.PLS,
875
+ client: mockFromClient,
876
+ });
877
+
878
+ expect(results).toBeDefined();
879
+ expect(Array.isArray(results)).toBe(true);
880
+ expect(results).toHaveLength(2);
881
+ expect(multicallRead).toHaveBeenCalledTimes(2);
882
+ });
883
+
884
+ it('should calculate maximum output in reduceResults', () => {
885
+ const helpers = fetchPriceCorrective({
886
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
887
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
888
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
889
+ assetOut: mockAssetOut,
890
+ fromChainClient: mockFromClient,
891
+ toChainClient: mockToClient,
892
+ assetLink: mockAssetLink,
893
+ amountIn: 1000000000000000000n,
894
+ isProd: true,
895
+ });
896
+
897
+ const mockResults: [[bigint[]], [bigint[]]] = [
898
+ [[1000000000000000000n, 950000000000000000n]], // outputs
899
+ [[1000000000000000000n, 980000000000000000n]], // inputs
900
+ ];
901
+
902
+ const result = helpers.reduceResults({
903
+ results: mockResults,
904
+ oneToken: 1000000000000000000n,
905
+ });
906
+
907
+ expect(result).toBeDefined();
908
+ expect(typeof result).toBe('bigint');
909
+ });
910
+
911
+ it('should handle failed router results in reduceResults', () => {
912
+ const helpers = fetchPriceCorrective({
913
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
914
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
915
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
916
+ assetOut: mockAssetOut,
917
+ fromChainClient: mockFromClient,
918
+ toChainClient: mockToClient,
919
+ assetLink: mockAssetLink,
920
+ amountIn: 1000000000000000000n,
921
+ isProd: true,
922
+ });
923
+
924
+ // Test with failed router results (Hex strings instead of bigint arrays)
925
+ const mockResults: [[Hex | bigint[]], [bigint[]]] = [
926
+ ['0xERROR' as Hex], // Failed router call returns Hex
927
+ [[1000000000000000000n, 980000000000000000n]], // inputs
928
+ ];
929
+
930
+ const result = helpers.reduceResults({
931
+ results: mockResults as any,
932
+ oneToken: 1000000000000000000n,
933
+ });
934
+
935
+ expect(result).toBeDefined();
936
+ // Should fall back to input token calculation
937
+ expect(result).toBeGreaterThan(0n);
938
+ });
939
+
940
+ it('should handle empty router results in reduceResults', () => {
941
+ const helpers = fetchPriceCorrective({
942
+ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH],
943
+ bridgeKeyPartner: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS],
944
+ measurementToken: '0x1234567890123456789012345678901234567890' as Hex,
945
+ assetOut: mockAssetOut,
946
+ fromChainClient: mockFromClient,
947
+ toChainClient: mockToClient,
948
+ assetLink: mockAssetLink,
949
+ amountIn: 1000000000000000000n,
950
+ isProd: true,
951
+ });
952
+
953
+ // Test with empty results
954
+ const mockResults: [[bigint[]], [bigint[]]] = [
955
+ [[]], // Empty output array
956
+ [[1000000000000000000n, 980000000000000000n]], // inputs
957
+ ];
958
+
959
+ const result = helpers.reduceResults({
960
+ results: mockResults,
961
+ oneToken: 1000000000000000000n,
962
+ });
963
+
964
+ expect(result).toBeDefined();
965
+ expect(result).toBeGreaterThan(0n);
966
+ });
967
+ });
968
+ });