@uniswap/token-list-bridge-utils 1.0.7 → 1.1.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,226 @@
1
+ import { AbiItem } from 'web3-utils'
2
+
3
+ const abi: AbiItem[] = [
4
+ {
5
+ constant: true,
6
+ inputs: [],
7
+ name: 'name',
8
+ outputs: [
9
+ {
10
+ name: '',
11
+ type: 'string',
12
+ },
13
+ ],
14
+ payable: false,
15
+ stateMutability: 'view',
16
+ type: 'function',
17
+ },
18
+ {
19
+ constant: false,
20
+ inputs: [
21
+ {
22
+ name: '_spender',
23
+ type: 'address',
24
+ },
25
+ {
26
+ name: '_value',
27
+ type: 'uint256',
28
+ },
29
+ ],
30
+ name: 'approve',
31
+ outputs: [
32
+ {
33
+ name: '',
34
+ type: 'bool',
35
+ },
36
+ ],
37
+ payable: false,
38
+ stateMutability: 'nonpayable',
39
+ type: 'function',
40
+ },
41
+ {
42
+ constant: true,
43
+ inputs: [],
44
+ name: 'totalSupply',
45
+ outputs: [
46
+ {
47
+ name: '',
48
+ type: 'uint256',
49
+ },
50
+ ],
51
+ payable: false,
52
+ stateMutability: 'view',
53
+ type: 'function',
54
+ },
55
+ {
56
+ constant: false,
57
+ inputs: [
58
+ {
59
+ name: '_from',
60
+ type: 'address',
61
+ },
62
+ {
63
+ name: '_to',
64
+ type: 'address',
65
+ },
66
+ {
67
+ name: '_value',
68
+ type: 'uint256',
69
+ },
70
+ ],
71
+ name: 'transferFrom',
72
+ outputs: [
73
+ {
74
+ name: '',
75
+ type: 'bool',
76
+ },
77
+ ],
78
+ payable: false,
79
+ stateMutability: 'nonpayable',
80
+ type: 'function',
81
+ },
82
+ {
83
+ constant: true,
84
+ inputs: [],
85
+ name: 'decimals',
86
+ outputs: [
87
+ {
88
+ name: '',
89
+ type: 'uint8',
90
+ },
91
+ ],
92
+ payable: false,
93
+ stateMutability: 'view',
94
+ type: 'function',
95
+ },
96
+ {
97
+ constant: true,
98
+ inputs: [
99
+ {
100
+ name: '_owner',
101
+ type: 'address',
102
+ },
103
+ ],
104
+ name: 'balanceOf',
105
+ outputs: [
106
+ {
107
+ name: 'balance',
108
+ type: 'uint256',
109
+ },
110
+ ],
111
+ payable: false,
112
+ stateMutability: 'view',
113
+ type: 'function',
114
+ },
115
+ {
116
+ constant: true,
117
+ inputs: [],
118
+ name: 'symbol',
119
+ outputs: [
120
+ {
121
+ name: '',
122
+ type: 'string',
123
+ },
124
+ ],
125
+ payable: false,
126
+ stateMutability: 'view',
127
+ type: 'function',
128
+ },
129
+ {
130
+ constant: false,
131
+ inputs: [
132
+ {
133
+ name: '_to',
134
+ type: 'address',
135
+ },
136
+ {
137
+ name: '_value',
138
+ type: 'uint256',
139
+ },
140
+ ],
141
+ name: 'transfer',
142
+ outputs: [
143
+ {
144
+ name: '',
145
+ type: 'bool',
146
+ },
147
+ ],
148
+ payable: false,
149
+ stateMutability: 'nonpayable',
150
+ type: 'function',
151
+ },
152
+ {
153
+ constant: true,
154
+ inputs: [
155
+ {
156
+ name: '_owner',
157
+ type: 'address',
158
+ },
159
+ {
160
+ name: '_spender',
161
+ type: 'address',
162
+ },
163
+ ],
164
+ name: 'allowance',
165
+ outputs: [
166
+ {
167
+ name: '',
168
+ type: 'uint256',
169
+ },
170
+ ],
171
+ payable: false,
172
+ stateMutability: 'view',
173
+ type: 'function',
174
+ },
175
+ {
176
+ payable: true,
177
+ stateMutability: 'payable',
178
+ type: 'fallback',
179
+ },
180
+ {
181
+ anonymous: false,
182
+ inputs: [
183
+ {
184
+ indexed: true,
185
+ name: 'owner',
186
+ type: 'address',
187
+ },
188
+ {
189
+ indexed: true,
190
+ name: 'spender',
191
+ type: 'address',
192
+ },
193
+ {
194
+ indexed: false,
195
+ name: 'value',
196
+ type: 'uint256',
197
+ },
198
+ ],
199
+ name: 'Approval',
200
+ type: 'event',
201
+ },
202
+ {
203
+ anonymous: false,
204
+ inputs: [
205
+ {
206
+ indexed: true,
207
+ name: 'from',
208
+ type: 'address',
209
+ },
210
+ {
211
+ indexed: true,
212
+ name: 'to',
213
+ type: 'address',
214
+ },
215
+ {
216
+ indexed: false,
217
+ name: 'value',
218
+ type: 'uint256',
219
+ },
220
+ ],
221
+ name: 'Transfer',
222
+ type: 'event',
223
+ },
224
+ ]
225
+
226
+ export default abi
@@ -1,6 +1,7 @@
1
1
  import { config } from 'dotenv'
2
+ import { ChainId } from '../constants/chainId'
3
+ import { getRpcUrl } from '../utils'
2
4
 
3
- // fall back to https://cloudflare-eth.com/ if MAINNET_RPC not provided in an .env file
5
+ // MAINNET_RPC value is required for arb-sdk. fall back to https://cloudflare-eth.com/ if MAINNET_RPC not provided in an .env file
4
6
  config()
5
- process.env.MAINNET_RPC =
6
- process.env.MAINNET_RPC || 'https://cloudflare-eth.com/'
7
+ process.env.MAINNET_RPC ??= getRpcUrl(ChainId.MAINNET)
@@ -10,4 +10,6 @@ export enum ChainId {
10
10
 
11
11
  POLYGON = 137,
12
12
  POLYGON_MUMBAI = 80001,
13
+
14
+ CELO = 42220,
13
15
  }
@@ -25,4 +25,8 @@ export interface PolygonMappedToken {
25
25
  updated_at: string
26
26
  uri: string
27
27
  }
28
- export type PolygonMappedTokenData = { [key: string]: PolygonMappedToken }
28
+
29
+ export type GenericMappedTokenData = { [key: string]: string | undefined }
30
+
31
+ // Polygon has its own type here since their api gives us more info than just the mapped token address
32
+ export type PolygonMappedTokenData = { [key: string]: PolygonMappedToken }
package/src/index.test.ts CHANGED
@@ -20,7 +20,7 @@ jest.setTimeout(15000)
20
20
  describe(chainifyTokenList, () => {
21
21
  it('outputs Optimism list correctly', async () => {
22
22
  const tokenList = await chainifyTokenList(
23
- ChainId.OPTIMISM,
23
+ [ChainId.OPTIMISM],
24
24
  sampleL1TokenList
25
25
  )
26
26
 
@@ -40,7 +40,7 @@ describe(chainifyTokenList, () => {
40
40
 
41
41
  it('outputs arbitrum list correctly', async () => {
42
42
  const tokenList = await chainifyTokenList(
43
- ChainId.ARBITRUM_ONE,
43
+ [ChainId.ARBITRUM_ONE],
44
44
  sampleL1TokenList
45
45
  )
46
46
 
@@ -64,7 +64,7 @@ describe(chainifyTokenList, () => {
64
64
 
65
65
  it('outputs polygon list correctly', async () => {
66
66
  const tokenList = await chainifyTokenList(
67
- ChainId.POLYGON,
67
+ [ChainId.POLYGON],
68
68
  sampleL1TokenList
69
69
  )
70
70
 
package/src/index.ts CHANGED
@@ -18,39 +18,33 @@ export async function chainify(
18
18
  l1TokenListOrPathOrUrl: TokenListOrFetchableTokenList
19
19
  ): Promise<TokenList> {
20
20
  const l1TokenList = await getTokenList(l1TokenListOrPathOrUrl)
21
-
22
- const optimisimed = await chainifyTokenList(
23
- ChainId.OPTIMISM,
24
- l1TokenListOrPathOrUrl
25
- )
26
- const polygoned = await chainifyTokenList(
21
+ const l2Chains = [
27
22
  ChainId.POLYGON,
28
- l1TokenListOrPathOrUrl
29
- )
30
- const arbified = await chainifyTokenList(
31
23
  ChainId.ARBITRUM_ONE,
32
- l1TokenListOrPathOrUrl
33
- )
24
+ ChainId.OPTIMISM,
25
+ ChainId.CELO,
26
+ ]
34
27
 
28
+ const chainified = await chainifyTokenList(l2Chains, l1TokenListOrPathOrUrl)
35
29
  return mergeTokenLists(
36
30
  l1TokenList, // providing l1 first to make sure duplicated tokens resolve to this list
37
- mergeTokenLists(mergeTokenLists(arbified, optimisimed), polygoned)
31
+ chainified
38
32
  )
39
33
  }
40
34
 
41
35
  /**
42
36
  * Given a network and a TokenList, returns the TokenList with `extensions` filled.
43
- * @param chainId chainId to operate on
37
+ * @param l2ChainIds layer 2 chainIds to operate on
44
38
  * @param l1TokenListOrPathOrUrl either an L1 TokenList object or a path/url to a TokenList
45
39
  * @returns L1 TokenList with `extensions` filled for the given network
46
40
  */
47
41
  export async function chainifyTokenList(
48
- chainId: ChainId,
42
+ l2ChainIds: Array<ChainId>,
49
43
  l1TokenListOrPathOrUrl: TokenListOrFetchableTokenList
50
44
  ): Promise<TokenList> {
51
45
  try {
52
46
  const l1TokenList = await getTokenList(l1TokenListOrPathOrUrl)
53
- const tokenList = await buildList(chainId, l1TokenList)
47
+ const tokenList = await buildList(l2ChainIds, l1TokenList)
54
48
  return verifyExtensions(tokenList)
55
49
  } catch (e) {
56
50
  throw new Error(`An error occured: ${e}`)
@@ -2,6 +2,7 @@ import { MappingProvider } from './MappingProvider'
2
2
  import { getNetworkConfig } from '../arbitrum/instantiate_bridge'
3
3
  import { getL2TokenAddressesFromL1 } from '../arbitrum/gateway'
4
4
  import { TokenList } from '@uniswap/token-lists'
5
+ import { GenericMappedTokenData } from '../constants/types'
5
6
 
6
7
  /**
7
8
  * This provider provides the l1->l2(Arbitrum) address mappings using the arbitrum SDK.
@@ -13,7 +14,7 @@ export class ArbitrumMappingProvider implements MappingProvider {
13
14
  this.l1TokenList = l1TokenList
14
15
  }
15
16
 
16
- async provide(): Promise<{ [key: string]: string | undefined }> {
17
+ async provide(): Promise<GenericMappedTokenData> {
17
18
  let tokens: { [key: string]: string | undefined } = {}
18
19
 
19
20
  const { l1, l2 } = await getNetworkConfig()
@@ -1,7 +1,7 @@
1
- import { PolygonMappedTokenData } from '../constants/types'
1
+ import { GenericMappedTokenData, PolygonMappedTokenData } from '../constants/types'
2
2
 
3
3
  export interface MappingProvider {
4
4
  provide(): Promise<
5
- PolygonMappedTokenData | { [key: string]: string | undefined }
5
+ PolygonMappedTokenData | GenericMappedTokenData
6
6
  >
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { MappingProvider } from './MappingProvider'
2
2
  import { ChainId } from '../constants/chainId'
3
3
  import { getTokenList } from '../utils'
4
+ import { GenericMappedTokenData } from '../constants/types'
4
5
 
5
6
  const optimismTokenListURL =
6
7
  'https://raw.githubusercontent.com/' +
@@ -12,7 +13,7 @@ const optimismTokenListURL =
12
13
  * This provider provides the l1->l2(Optimism) token mappings.
13
14
  */
14
15
  export class OptimismMappingProvider implements MappingProvider {
15
- async provide(): Promise<{ [key: string]: string | undefined }> {
16
+ async provide(): Promise<GenericMappedTokenData> {
16
17
  const tokens: { [key: string]: string | undefined } = {}
17
18
 
18
19
  let optimismTokens = await getTokenList(optimismTokenListURL)
@@ -1,88 +1,133 @@
1
1
  import { ChainId } from '../constants/chainId'
2
+ import { Contract } from 'web3-eth-contract'
2
3
  import { ArbitrumMappingProvider } from './ArbitrumMappingProvider'
3
4
  import { OptimismMappingProvider } from './OptimismMappingProvider'
4
5
  import { PolygonMappingProvider } from './PolygonMappingProvider'
5
6
  import { TokenInfo, TokenList } from '@uniswap/token-lists'
6
- import { compareTokenInfos } from '../utils'
7
7
  import { ethers } from 'ethers'
8
+ import {
9
+ compareTokenInfos,
10
+ getRpcUrl,
11
+ getTokenSymbolFromContract,
12
+ } from '../utils'
13
+ import ERC20Abi from '../abis/erc20'
14
+ // TODO: use ethers for contract calls (without performance reduction).
15
+ import Web3 from 'web3'
16
+ import {
17
+ GenericMappedTokenData,
18
+ PolygonMappedTokenData,
19
+ } from '../constants/types'
20
+
21
+ const web3 = new Web3()
22
+
23
+ // chains we support fetching mappings for (can be different than the l2ChainIds arg for buildList)
24
+ const SUPPORTED_L2_CHAINS = [
25
+ ChainId.ARBITRUM_ONE,
26
+ ChainId.POLYGON,
27
+ ChainId.OPTIMISM,
28
+ ]
8
29
 
9
30
  export async function buildList(
10
- chainId: ChainId,
31
+ l2ChainIds: Array<ChainId>,
11
32
  l1TokenList: TokenList
12
33
  ): Promise<TokenList> {
13
- const tokenAddressMap = await getMappingProvider(
14
- chainId,
34
+ const multiChainedTokens: TokenInfo[] = []
35
+ const chainIdToMappingsMap = await generateTokenMappings(
36
+ l2ChainIds,
15
37
  l1TokenList
16
- ).provide()
17
- const mappedTokens = []
18
- for (const rootToken of l1TokenList.tokens) {
19
- const childToken = tokenAddressMap[rootToken.address.toLowerCase()]
20
- const childTokenValid = Boolean(
21
- childToken &&
22
- (typeof childToken === 'object' ? !childToken.deleted : true)
23
- )
24
-
25
- if (rootToken.chainId === ChainId.MAINNET) {
26
- // build extension info if available
27
- const toRootExtensions = childTokenValid
28
- ? {
29
- extensions: {
30
- bridgeInfo: {
31
- [rootToken.chainId]: {
32
- tokenAddress: ethers.utils.getAddress(rootToken.address),
33
- },
34
- },
35
- },
36
- }
37
- : {}
38
-
39
- const toChildExtensions = childTokenValid
40
- ? {
41
- extensions: {
42
- bridgeInfo: {
43
- [chainId]: {
44
- tokenAddress: ethers.utils.getAddress(
45
- typeof childToken === 'object'
46
- ? childToken.child_token
47
- : childToken!
48
- ),
49
- },
50
- },
51
- },
38
+ )
39
+ for (const l1Token of l1TokenList.tokens) {
40
+ if (l1Token.chainId === ChainId.MAINNET) {
41
+ const chainIdToChildTokenDetailsMap: {
42
+ [key: number]: {
43
+ childTokenValid: boolean
44
+ childTokenAddress: string | undefined
45
+ }
46
+ } = {}
47
+ const l2MappingExtension = {
48
+ extensions: {
49
+ bridgeInfo: {},
50
+ },
51
+ }
52
+ // build out the extensions.bridgeInfo data containing mappings for each L2 chain
53
+ await Promise.all(
54
+ l2ChainIds.map(async (chainId) => {
55
+ chainIdToChildTokenDetailsMap[chainId] = await getChildTokenDetails(
56
+ l1Token,
57
+ chainId,
58
+ chainIdToMappingsMap
59
+ )
60
+ if (chainIdToChildTokenDetailsMap[chainId].childTokenValid) {
61
+ l2MappingExtension.extensions.bridgeInfo[chainId] = {
62
+ tokenAddress:
63
+ chainIdToChildTokenDetailsMap[chainId].childTokenAddress,
64
+ }
52
65
  }
53
- : {}
66
+ })
67
+ )
54
68
 
55
- const rootTokenInfo: TokenInfo = {
56
- ...rootToken,
57
- ...toChildExtensions,
58
- } as unknown as TokenInfo
69
+ // build the TokenInfo objects with bridgeInfo extension
70
+ l2ChainIds.concat([ChainId.MAINNET]).forEach((chainId) => {
71
+ if (
72
+ chainId === ChainId.MAINNET ||
73
+ chainIdToChildTokenDetailsMap[chainId].childTokenValid
74
+ ) {
75
+ const tokenInfo: TokenInfo =
76
+ chainId === ChainId.MAINNET
77
+ ? ({
78
+ ...l1Token,
79
+ extensions:
80
+ Object.keys(l2MappingExtension.extensions.bridgeInfo)
81
+ .length > 0
82
+ ? {
83
+ bridgeInfo: l2MappingExtension.extensions.bridgeInfo,
84
+ }
85
+ : undefined,
86
+ } as unknown as TokenInfo)
87
+ : ({
88
+ ...l1Token,
89
+ chainId: chainId,
90
+ address:
91
+ chainIdToChildTokenDetailsMap[chainId].childTokenAddress,
92
+ extensions: {
93
+ bridgeInfo: {
94
+ [ChainId.MAINNET]: {
95
+ tokenAddress: ethers.utils.getAddress(l1Token.address),
96
+ },
97
+ },
98
+ },
99
+ } as unknown as TokenInfo)
59
100
 
60
- if (childTokenValid) {
61
- const childTokenInfo: TokenInfo = {
62
- ...rootToken,
63
- chainId: chainId,
64
- address: ethers.utils.getAddress(
65
- typeof childToken === 'object'
66
- ? childToken.child_token
67
- : childToken!
68
- ),
69
- ...toRootExtensions,
70
- } as unknown as TokenInfo
71
- mappedTokens.push(childTokenInfo)
72
- }
73
- mappedTokens.push(rootTokenInfo)
101
+ multiChainedTokens.push(tokenInfo)
102
+ }
103
+ })
74
104
  }
75
105
  }
106
+
107
+ // build and return final chainified token list
76
108
  const tokenList = {
77
- name: `(ChainId: ${chainId}) ${l1TokenList.name}`,
109
+ name: `(ChainIds: ${l2ChainIds}) ${l1TokenList.name}`,
78
110
  timestamp: new Date().toISOString(),
79
111
  version: l1TokenList.version,
80
- tokens: mappedTokens.sort(compareTokenInfos),
112
+ tokens: multiChainedTokens.sort(compareTokenInfos),
81
113
  }
82
114
 
83
115
  return tokenList
84
116
  }
85
117
 
118
+ // using a symbol lookup contract call to check whether the token exists on the L2
119
+ async function hasExistingTokenContract(address: string, chainId: ChainId) {
120
+ web3.setProvider(getRpcUrl(chainId))
121
+ try {
122
+ const contract: Contract = new web3.eth.Contract(ERC20Abi, address)
123
+ await getTokenSymbolFromContract(contract)
124
+ } catch {
125
+ return false
126
+ }
127
+
128
+ return true
129
+ }
130
+
86
131
  function getMappingProvider(chainId: ChainId, l1TokenList: TokenList) {
87
132
  switch (chainId) {
88
133
  case ChainId.ARBITRUM_ONE:
@@ -92,6 +137,66 @@ function getMappingProvider(chainId: ChainId, l1TokenList: TokenList) {
92
137
  case ChainId.POLYGON:
93
138
  return new PolygonMappingProvider()
94
139
  default:
95
- throw new Error(`Chain ${chainId} not supported.`)
140
+ throw new Error(`Chain ${chainId} not supported for fetching mappings.`)
141
+ }
142
+ }
143
+
144
+ async function generateTokenMappings(
145
+ chainIds: ChainId[],
146
+ l1TokenList: TokenList
147
+ ) {
148
+ const chainIdToMappingsMap: {
149
+ [key: number]: PolygonMappedTokenData | GenericMappedTokenData
150
+ } = {}
151
+
152
+ for (const chainId of chainIds) {
153
+ if (SUPPORTED_L2_CHAINS.includes(chainId)) {
154
+ chainIdToMappingsMap[chainId] = await getMappingProvider(
155
+ chainId,
156
+ l1TokenList
157
+ ).provide()
158
+ }
159
+ }
160
+
161
+ return chainIdToMappingsMap
162
+ }
163
+
164
+ // handles both string and object cases for childToken (Polygon mappings return object)
165
+ async function getChildTokenDetails(
166
+ l1Token: TokenInfo,
167
+ chainId: ChainId,
168
+ chainIdToMappingsMap: {
169
+ [key: number]: PolygonMappedTokenData | GenericMappedTokenData
170
+ }
171
+ ): Promise<{
172
+ childTokenValid: boolean
173
+ childTokenAddress: string | undefined
174
+ }> {
175
+ const existingMapping: undefined | string =
176
+ l1Token?.extensions?.bridgeInfo?.[chainId]?.tokenAddress
177
+ // use the externally fetched mappings if manual entry doesn't exist for the token/chain mapping
178
+ // and the given L2 chain is supported for fetching mappings
179
+ if (SUPPORTED_L2_CHAINS.includes(chainId) && existingMapping === undefined) {
180
+ const childToken =
181
+ chainIdToMappingsMap[chainId][l1Token.address.toLowerCase()]
182
+
183
+ const childTokenAddress = childToken
184
+ ? ethers.utils.getAddress(
185
+ typeof childToken === 'object' ? childToken.child_token : childToken
186
+ )
187
+ : undefined
188
+ const childTokenValid = Boolean(
189
+ childTokenAddress &&
190
+ (typeof childToken === 'object' ? !childToken.deleted : true) &&
191
+ (await hasExistingTokenContract(childTokenAddress, chainId))
192
+ )
193
+ return {
194
+ childTokenValid: childTokenValid,
195
+ childTokenAddress: childTokenAddress,
196
+ }
197
+ }
198
+ return {
199
+ childTokenValid: !!existingMapping,
200
+ childTokenAddress: existingMapping,
96
201
  }
97
202
  }
package/src/utils.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { TokenInfo, TokenList } from '@uniswap/token-lists'
2
2
  import { readFileSync, existsSync } from 'fs'
3
3
  import axios from 'axios'
4
+ import { ChainId } from './constants/chainId'
5
+ import { Contract } from 'web3-eth-contract'
4
6
 
5
7
  export type TokenListOrFetchableTokenList = TokenList | string
6
8
 
@@ -89,3 +91,29 @@ export const isTokenList = (obj: any) => {
89
91
  throw new Error('tokenlist typeguard error: token missing required key')
90
92
  }
91
93
  }
94
+
95
+ export function getRpcUrl(chainId: ChainId): string {
96
+ switch (chainId) {
97
+ case ChainId.MAINNET:
98
+ return 'https://cloudflare-eth.com/'
99
+ case ChainId.OPTIMISM:
100
+ return 'https://rpc.ankr.com/optimism' // seems to have higher rate limit than https://mainnet.optimism.io/
101
+ case ChainId.OPTIMISTIC_KOVAN:
102
+ return 'https://kovan.optimism.io'
103
+ case ChainId.ARBITRUM_ONE:
104
+ return 'https://arb1.arbitrum.io/rpc'
105
+ case ChainId.ARBITRUM_RINKEBY:
106
+ return 'https://rinkeby.arbitrum.io/rpc'
107
+ case ChainId.POLYGON:
108
+ return 'https://polygon-rpc.com/'
109
+ case ChainId.POLYGON_MUMBAI:
110
+ return 'https://rpc-endpoints.superfluid.dev/mumbai'
111
+ default:
112
+ }
113
+ throw new Error('Unsupported ChainId')
114
+ }
115
+
116
+ export async function getTokenSymbolFromContract(tokenContract: Contract) {
117
+ const symbol = await Promise.all([tokenContract.methods.symbol().call()])
118
+ return symbol
119
+ }