@uniswap/token-list-bridge-utils 2.10.0 → 2.11.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,124 @@
1
+ import { isTokenExcluded, EXCLUDED_TOKENS } from './excludedTokens'
2
+ import { ChainId } from './chainId'
3
+
4
+ describe('excludedTokens', () => {
5
+ describe('isTokenExcluded', () => {
6
+ it('returns true for excluded tokens on Arbitrum', () => {
7
+ expect(
8
+ isTokenExcluded(
9
+ ChainId.ARBITRUM_ONE,
10
+ '0xfEb4DfC8C4Cf7Ed305bb08065D08eC6ee6728429'
11
+ )
12
+ ).toBe(true)
13
+ })
14
+
15
+ it('returns true for excluded tokens on Polygon', () => {
16
+ expect(
17
+ isTokenExcluded(
18
+ ChainId.POLYGON,
19
+ '0x553d3D295e0f695B9228246232eDF400ed3560B5'
20
+ )
21
+ ).toBe(true)
22
+ })
23
+
24
+ it('returns true for excluded tokens on Unichain', () => {
25
+ expect(
26
+ isTokenExcluded(
27
+ ChainId.UNICHAIN,
28
+ '0x89f7C0870794103744C8042630CC1C846a858E57'
29
+ )
30
+ ).toBe(true)
31
+ })
32
+
33
+ it('is case-insensitive for token addresses', () => {
34
+ expect(
35
+ isTokenExcluded(
36
+ ChainId.ARBITRUM_ONE,
37
+ '0xFEB4DFC8C4CF7ED305BB08065D08EC6EE6728429'
38
+ )
39
+ ).toBe(true)
40
+
41
+ expect(
42
+ isTokenExcluded(
43
+ ChainId.POLYGON,
44
+ '0X553D3D295E0F695B9228246232EDF400ED3560B5'
45
+ )
46
+ ).toBe(true)
47
+ })
48
+
49
+ it('returns false for non-excluded tokens', () => {
50
+ expect(
51
+ isTokenExcluded(
52
+ ChainId.ARBITRUM_ONE,
53
+ '0x0000000000000000000000000000000000000000'
54
+ )
55
+ ).toBe(false)
56
+
57
+ expect(
58
+ isTokenExcluded(
59
+ ChainId.POLYGON,
60
+ '0x0000000000000000000000000000000000000000'
61
+ )
62
+ ).toBe(false)
63
+ })
64
+
65
+ it('returns false for chains without exclusions', () => {
66
+ expect(
67
+ isTokenExcluded(
68
+ ChainId.OPTIMISM,
69
+ '0xfEb4DfC8C4Cf7Ed305bb08065D08eC6ee6728429'
70
+ )
71
+ ).toBe(false)
72
+
73
+ expect(
74
+ isTokenExcluded(
75
+ ChainId.BASE,
76
+ '0x553d3D295e0f695B9228246232eDF400ed3560B5'
77
+ )
78
+ ).toBe(false)
79
+ })
80
+
81
+ it('returns false when excluded token is checked against wrong chain', () => {
82
+ // Arbitrum excluded token checked on Polygon
83
+ expect(
84
+ isTokenExcluded(
85
+ ChainId.POLYGON,
86
+ '0xfEb4DfC8C4Cf7Ed305bb08065D08eC6ee6728429'
87
+ )
88
+ ).toBe(false)
89
+
90
+ // Polygon excluded token checked on Arbitrum
91
+ expect(
92
+ isTokenExcluded(
93
+ ChainId.ARBITRUM_ONE,
94
+ '0x553d3D295e0f695B9228246232eDF400ed3560B5'
95
+ )
96
+ ).toBe(false)
97
+ })
98
+ })
99
+
100
+ describe('EXCLUDED_TOKENS', () => {
101
+ it('contains the correct chains', () => {
102
+ expect(EXCLUDED_TOKENS[ChainId.ARBITRUM_ONE]).toBeDefined()
103
+ expect(EXCLUDED_TOKENS[ChainId.POLYGON]).toBeDefined()
104
+ expect(EXCLUDED_TOKENS[ChainId.UNICHAIN]).toBeDefined()
105
+ })
106
+
107
+ it('stores addresses in lowercase', () => {
108
+ const arbitrumExcluded = Array.from(EXCLUDED_TOKENS[ChainId.ARBITRUM_ONE])
109
+ expect(
110
+ arbitrumExcluded.every((addr) => addr === addr.toLowerCase())
111
+ ).toBe(true)
112
+
113
+ const polygonExcluded = Array.from(EXCLUDED_TOKENS[ChainId.POLYGON])
114
+ expect(polygonExcluded.every((addr) => addr === addr.toLowerCase())).toBe(
115
+ true
116
+ )
117
+
118
+ const unichainExcluded = Array.from(EXCLUDED_TOKENS[ChainId.UNICHAIN])
119
+ expect(
120
+ unichainExcluded.every((addr) => addr === addr.toLowerCase())
121
+ ).toBe(true)
122
+ })
123
+ })
124
+ })
@@ -0,0 +1,25 @@
1
+ import { ChainId } from './chainId'
2
+
3
+ /**
4
+ * Tokens to exclude from being added to the token list.
5
+ * Key: chainId, Value: Set of lowercase token addresses to exclude on that chain
6
+ */
7
+ export const EXCLUDED_TOKENS: { [chainId: number]: Set<string> } = {
8
+ [ChainId.ARBITRUM_ONE]: new Set([
9
+ '0xfeb4dfc8c4cf7ed305bb08065d08ec6ee6728429',
10
+ ]),
11
+ [ChainId.POLYGON]: new Set(['0x553d3d295e0f695b9228246232edf400ed3560b5']),
12
+ [ChainId.UNICHAIN]: new Set(['0x89f7c0870794103744c8042630cc1c846a858e57']),
13
+ }
14
+
15
+ /**
16
+ * Check if a token address should be excluded on a given chain
17
+ */
18
+ export function isTokenExcluded(
19
+ chainId: number,
20
+ tokenAddress: string
21
+ ): boolean {
22
+ const excludedSet = EXCLUDED_TOKENS[chainId]
23
+ if (!excludedSet) return false
24
+ return excludedSet.has(tokenAddress.toLowerCase())
25
+ }
package/src/index.test.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { TokenInfo } from '@uniswap/token-lists'
1
+ import { TokenInfo, TokenList } from '@uniswap/token-lists'
2
+ import { Token } from '@uniswap/sdk-core'
2
3
  import { chainify, chainifyTokenList, mergeTokenLists } from '.'
3
4
  import { ChainId } from './constants/chainId'
4
5
  import {
@@ -457,3 +458,152 @@ describe(chainify, () => {
457
458
  ])
458
459
  })
459
460
  })
461
+
462
+ describe('excluded tokens', () => {
463
+ it('excludes tokens from Arbitrum when specified in EXCLUDED_TOKENS', async () => {
464
+ // Use a token that has the excluded L2 address as its explicit bridgeInfo
465
+ const testTokenList: TokenList = {
466
+ name: 'Test Exclusion',
467
+ timestamp: new Date().toISOString(),
468
+ version: { major: 1, minor: 0, patch: 0 },
469
+ tokens: [
470
+ {
471
+ chainId: ChainId.MAINNET,
472
+ address: '0x0000000000000000000000000000000000000001',
473
+ name: 'Test Token',
474
+ symbol: 'TEST',
475
+ decimals: 18,
476
+ extensions: {
477
+ bridgeInfo: {
478
+ [ChainId.ARBITRUM_ONE]: {
479
+ tokenAddress: '0xfEb4DfC8C4Cf7Ed305bb08065D08eC6ee6728429', // excluded arbitrum address
480
+ },
481
+ },
482
+ },
483
+ },
484
+ ],
485
+ }
486
+
487
+ const chainified = await chainifyTokenList(
488
+ [ChainId.ARBITRUM_ONE],
489
+ testTokenList
490
+ )
491
+
492
+ // Should only contain mainnet token, not the Arbitrum L2 token
493
+ expect(chainified.tokens.length).toBe(1)
494
+ expect(chainified.tokens[0].chainId).toBe(ChainId.MAINNET)
495
+ // Should not have bridgeInfo for Arbitrum since L2 token is excluded
496
+ expect(chainified.tokens[0].extensions?.bridgeInfo).toBeUndefined()
497
+ })
498
+
499
+ it('excludes tokens from Polygon when specified in EXCLUDED_TOKENS', async () => {
500
+ // Use DAI which has a valid Polygon mapping
501
+ const testTokenList: TokenList = {
502
+ name: 'Test Polygon Exclusion',
503
+ timestamp: new Date().toISOString(),
504
+ version: { major: 1, minor: 0, patch: 0 },
505
+ tokens: [
506
+ {
507
+ chainId: ChainId.MAINNET,
508
+ address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
509
+ name: 'Dai Stablecoin',
510
+ symbol: 'DAI',
511
+ decimals: 18,
512
+ extensions: {
513
+ bridgeInfo: {
514
+ [ChainId.POLYGON]: {
515
+ tokenAddress: '0x553d3D295e0f695B9228246232eDF400ed3560B5', // excluded polygon address
516
+ },
517
+ },
518
+ },
519
+ },
520
+ ],
521
+ }
522
+
523
+ const chainified = await chainifyTokenList([ChainId.POLYGON], testTokenList)
524
+
525
+ // Should only contain mainnet token, not the Polygon L2 token
526
+ expect(chainified.tokens.length).toBe(1)
527
+ expect(chainified.tokens[0].chainId).toBe(ChainId.MAINNET)
528
+ // Should not have bridgeInfo for Polygon since the L2 token is excluded
529
+ expect(chainified.tokens[0].extensions?.bridgeInfo).toBeUndefined()
530
+ })
531
+
532
+ it('excludes tokens from Unichain when specified in EXCLUDED_TOKENS', async () => {
533
+ // Create a token list with the excluded Unichain token
534
+ const EXCLUDED_TOKEN_MAINNET = new Token(
535
+ ChainId.MAINNET,
536
+ '0x45804880de22913dafe09f4980848ece6ecbaf78', // PAX Gold - maps to excluded unichain token
537
+ 18,
538
+ 'PAXG',
539
+ 'PAX Gold'
540
+ )
541
+
542
+ const testTokenList: TokenList = {
543
+ name: 'Test Unichain Exclusion',
544
+ timestamp: new Date().toISOString(),
545
+ version: { major: 1, minor: 0, patch: 0 },
546
+ tokens: [
547
+ {
548
+ chainId: ChainId.MAINNET,
549
+ address: EXCLUDED_TOKEN_MAINNET.address,
550
+ name: EXCLUDED_TOKEN_MAINNET.name!,
551
+ symbol: EXCLUDED_TOKEN_MAINNET.symbol!,
552
+ decimals: EXCLUDED_TOKEN_MAINNET.decimals,
553
+ },
554
+ ],
555
+ }
556
+
557
+ const chainified = await chainifyTokenList(
558
+ [ChainId.UNICHAIN],
559
+ testTokenList
560
+ )
561
+
562
+ // Should only contain mainnet token, not the Unichain L2 token
563
+ expect(chainified.tokens.length).toBe(1)
564
+ expect(chainified.tokens[0].chainId).toBe(ChainId.MAINNET)
565
+ expect(chainified.tokens[0].address.toLowerCase()).toBe(
566
+ EXCLUDED_TOKEN_MAINNET.address.toLowerCase()
567
+ )
568
+ // Should not have bridgeInfo for Unichain
569
+ expect(chainified.tokens[0].extensions?.bridgeInfo).toBeUndefined()
570
+ })
571
+
572
+ it('does not exclude tokens on non-excluded chains', async () => {
573
+ // Use the same mainnet address that maps to excluded Arbitrum token
574
+ // but chainify for Optimism instead
575
+ const TOKEN_MAINNET = new Token(
576
+ ChainId.MAINNET,
577
+ '0x8290333cef9e6d528dd5618fb97a76f268f3edd4',
578
+ 18,
579
+ 'ANKR',
580
+ 'Ankr Network'
581
+ )
582
+
583
+ const testTokenList: TokenList = {
584
+ name: 'Test Non-Exclusion',
585
+ timestamp: new Date().toISOString(),
586
+ version: { major: 1, minor: 0, patch: 0 },
587
+ tokens: [
588
+ {
589
+ chainId: ChainId.MAINNET,
590
+ address: TOKEN_MAINNET.address,
591
+ name: TOKEN_MAINNET.name!,
592
+ symbol: TOKEN_MAINNET.symbol!,
593
+ decimals: TOKEN_MAINNET.decimals,
594
+ },
595
+ ],
596
+ }
597
+
598
+ const chainified = await chainifyTokenList(
599
+ [ChainId.OPTIMISM],
600
+ testTokenList
601
+ )
602
+
603
+ // May contain L2 tokens if valid mappings exist for Optimism
604
+ const mainnetToken = chainified.tokens.find(
605
+ (t) => t.chainId === ChainId.MAINNET
606
+ )
607
+ expect(mainnetToken).toBeDefined()
608
+ })
609
+ })
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  } from './utils'
10
10
  import { verifyExtensions } from './verify'
11
11
 
12
+ export { EXCLUDED_TOKENS, isTokenExcluded } from './constants/excludedTokens'
13
+
12
14
  /**
13
15
  * Adds bridgeInfo to the given token list for Optimism, Polygon and Arbitrum.
14
16
  * @param l1TokenListOrPathOrUrl
@@ -25,6 +25,7 @@ import { AvalancheMappingProvider } from './AvalancheMappingProvider'
25
25
  import { BaseMappingProvider } from './BaseMappingProvider'
26
26
  import { CeloMappingProvider } from './CeloMappingProvider'
27
27
  import { SoneiumMappingProvider } from './SoneiumMappingProvider'
28
+ import { isTokenExcluded } from '../constants/excludedTokens'
28
29
 
29
30
  const web3 = new Web3()
30
31
 
@@ -72,10 +73,17 @@ export async function buildList(
72
73
  chainId,
73
74
  chainIdToMappingsMap
74
75
  )
75
- if (chainIdToChildTokenDetailsMap[chainId].childTokenValid) {
76
+ const childTokenAddress =
77
+ chainIdToChildTokenDetailsMap[chainId].childTokenAddress
78
+
79
+ // Only add bridgeInfo if child token is valid and not excluded
80
+ if (
81
+ chainIdToChildTokenDetailsMap[chainId].childTokenValid &&
82
+ childTokenAddress &&
83
+ !isTokenExcluded(chainId, childTokenAddress)
84
+ ) {
76
85
  l2MappingExtension.extensions.bridgeInfo[chainId] = {
77
- tokenAddress:
78
- chainIdToChildTokenDetailsMap[chainId].childTokenAddress,
86
+ tokenAddress: childTokenAddress,
79
87
  }
80
88
  }
81
89
  })
@@ -83,6 +91,16 @@ export async function buildList(
83
91
 
84
92
  // build the TokenInfo objects with bridgeInfo extension
85
93
  l2ChainIds.concat([ChainId.MAINNET]).forEach((chainId) => {
94
+ const tokenAddress =
95
+ chainId === ChainId.MAINNET
96
+ ? l1Token.address
97
+ : chainIdToChildTokenDetailsMap[chainId]?.childTokenAddress
98
+
99
+ // Skip if token is excluded on this chain
100
+ if (tokenAddress && isTokenExcluded(chainId, tokenAddress)) {
101
+ return
102
+ }
103
+
86
104
  if (
87
105
  chainId === ChainId.MAINNET ||
88
106
  chainIdToChildTokenDetailsMap[chainId].childTokenValid