@uniswap/token-list-bridge-utils 2.10.0 → 2.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,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
@@ -28,6 +30,7 @@ export async function chainify(
28
30
  ChainId.BASE,
29
31
  ChainId.UNICHAIN,
30
32
  ChainId.SONEIUM,
33
+ ChainId.MONAD,
31
34
  ]
32
35
 
33
36
  const chainified = await chainifyTokenList(l2Chains, l1TokenListOrPathOrUrl)
@@ -0,0 +1,22 @@
1
+ {
2
+ "0xdac17f958d2ee523a2206206994597c13d831ec7": {
3
+ "childToken": "0xe7cd86e13AC4309349F30B3435a9d337750fC82D",
4
+ "decimals": 6
5
+ },
6
+ "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
7
+ "childToken": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
8
+ "decimals": 8
9
+ },
10
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
11
+ "childToken": "0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242",
12
+ "decimals": 18
13
+ },
14
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": {
15
+ "childToken": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603",
16
+ "decimals": 6
17
+ },
18
+ "0xd31a59c85ae9d8edefec411d448f90841571b89c": {
19
+ "childToken": "0xea17E5a9efEBf1477dB45082d67010E2245217f1",
20
+ "decimals": 9
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ import { MappingProvider } from './MappingProvider'
2
+ import { MappedTokenData } from '../constants/types'
3
+ import monadMappings from '../local_mappings/monad.json'
4
+
5
+ // we use a local source of mappings for monad
6
+ export class MonadMappingProvider implements MappingProvider {
7
+ async provide(): Promise<MappedTokenData> {
8
+ const tokens = monadMappings as MappedTokenData
9
+ return tokens
10
+ }
11
+ }
@@ -5,6 +5,7 @@ import { OptimismMappingProvider } from './OptimismMappingProvider'
5
5
  import { PolygonMappingProvider } from './PolygonMappingProvider'
6
6
  import { BnbMappingProvider } from './BnbMappingProvider'
7
7
  import { UnichainMappingProvider } from './UnichainMappingProvider'
8
+ import { MonadMappingProvider } from './MonadMappingProvider'
8
9
  import { TokenInfo, TokenList } from '@uniswap/token-lists'
9
10
  import { ethers } from 'ethers'
10
11
  import {
@@ -25,6 +26,7 @@ import { AvalancheMappingProvider } from './AvalancheMappingProvider'
25
26
  import { BaseMappingProvider } from './BaseMappingProvider'
26
27
  import { CeloMappingProvider } from './CeloMappingProvider'
27
28
  import { SoneiumMappingProvider } from './SoneiumMappingProvider'
29
+ import { isTokenExcluded } from '../constants/excludedTokens'
28
30
 
29
31
  const web3 = new Web3()
30
32
 
@@ -39,6 +41,7 @@ const CHAINS_WITH_MAPPING_PROVIDERS = [
39
41
  ChainId.BASE,
40
42
  ChainId.UNICHAIN,
41
43
  ChainId.SONEIUM,
44
+ ChainId.MONAD,
42
45
  ]
43
46
 
44
47
  export async function buildList(
@@ -72,10 +75,17 @@ export async function buildList(
72
75
  chainId,
73
76
  chainIdToMappingsMap
74
77
  )
75
- if (chainIdToChildTokenDetailsMap[chainId].childTokenValid) {
78
+ const childTokenAddress =
79
+ chainIdToChildTokenDetailsMap[chainId].childTokenAddress
80
+
81
+ // Only add bridgeInfo if child token is valid and not excluded
82
+ if (
83
+ chainIdToChildTokenDetailsMap[chainId].childTokenValid &&
84
+ childTokenAddress &&
85
+ !isTokenExcluded(chainId, childTokenAddress)
86
+ ) {
76
87
  l2MappingExtension.extensions.bridgeInfo[chainId] = {
77
- tokenAddress:
78
- chainIdToChildTokenDetailsMap[chainId].childTokenAddress,
88
+ tokenAddress: childTokenAddress,
79
89
  }
80
90
  }
81
91
  })
@@ -83,6 +93,16 @@ export async function buildList(
83
93
 
84
94
  // build the TokenInfo objects with bridgeInfo extension
85
95
  l2ChainIds.concat([ChainId.MAINNET]).forEach((chainId) => {
96
+ const tokenAddress =
97
+ chainId === ChainId.MAINNET
98
+ ? l1Token.address
99
+ : chainIdToChildTokenDetailsMap[chainId]?.childTokenAddress
100
+
101
+ // Skip if token is excluded on this chain
102
+ if (tokenAddress && isTokenExcluded(chainId, tokenAddress)) {
103
+ return
104
+ }
105
+
86
106
  if (
87
107
  chainId === ChainId.MAINNET ||
88
108
  chainIdToChildTokenDetailsMap[chainId].childTokenValid
@@ -166,6 +186,8 @@ function getMappingProvider(chainId: ChainId, l1TokenList: TokenList) {
166
186
  return new UnichainMappingProvider()
167
187
  case ChainId.SONEIUM:
168
188
  return new SoneiumMappingProvider()
189
+ case ChainId.MONAD:
190
+ return new MonadMappingProvider()
169
191
  default:
170
192
  throw new Error(`Chain ${chainId} not supported for fetching mappings.`)
171
193
  }
@@ -233,7 +255,7 @@ async function getChildTokenDetails(
233
255
  : undefined
234
256
  const childTokenValid = Boolean(
235
257
  childTokenAddress &&
236
- (chainId === ChainId.UNICHAIN // barring a unichain rpc url, we skip the contract call. Since we're using a manually curated list of tokens, we can assume they exist.
258
+ (chainId === ChainId.UNICHAIN || chainId === ChainId.MONAD // barring a unichain/monad rpc url, we skip the contract call. Since we're using a manually curated list of tokens, we can assume they exist.
237
259
  ? true
238
260
  : await hasExistingTokenContract(childTokenAddress, chainId))
239
261
  )
package/src/utils.ts CHANGED
@@ -97,7 +97,7 @@ export function getRpcUrl(chainId: ChainId): string {
97
97
  case ChainId.MAINNET:
98
98
  return 'https://ethereum-rpc.publicnode.com'
99
99
  case ChainId.OPTIMISM:
100
- return 'https://rpc.ankr.com/optimism' // seems to have higher rate limit than https://mainnet.optimism.io/
100
+ return 'https://mainnet.optimism.io'
101
101
  case ChainId.OPTIMISTIC_KOVAN:
102
102
  return 'https://kovan.optimism.io'
103
103
  case ChainId.ARBITRUM_ONE: