@uniswap/token-list-bridge-utils 0.1.1-beta.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 (61) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +17 -0
  3. package/dist/arb-token-lists/server/app.d.ts +1 -0
  4. package/dist/arb-token-lists/src/lib/getClargs.d.ts +8 -0
  5. package/dist/arb-token-lists/src/lib/graph.d.ts +2 -0
  6. package/dist/arb-token-lists/src/lib/instantiate_bridge.d.ts +15 -0
  7. package/dist/arb-token-lists/src/lib/token_list_gen.d.ts +23 -0
  8. package/dist/arb-token-lists/src/lib/types.d.ts +36 -0
  9. package/dist/arb-token-lists/src/lib/utils.d.ts +18 -0
  10. package/dist/arb-token-lists/src/main.d.ts +1 -0
  11. package/dist/constants/chainId.d.ts +10 -0
  12. package/dist/constants/tokens.d.ts +5 -0
  13. package/dist/fixtures/index.d.ts +24 -0
  14. package/dist/index.d.ts +18 -0
  15. package/dist/index.js +8 -0
  16. package/dist/providers/ArbitrumMappingProvider.d.ts +11 -0
  17. package/dist/providers/MappingProvider.d.ts +4 -0
  18. package/dist/providers/OptimismMappingProvider.d.ts +10 -0
  19. package/dist/providers/PolygonMappingProvider.d.ts +9 -0
  20. package/dist/providers/index.d.ts +5 -0
  21. package/dist/token-list-bridge-utils.cjs.development.js +2217 -0
  22. package/dist/token-list-bridge-utils.cjs.development.js.map +1 -0
  23. package/dist/token-list-bridge-utils.cjs.production.min.js +2 -0
  24. package/dist/token-list-bridge-utils.cjs.production.min.js.map +1 -0
  25. package/dist/token-list-bridge-utils.esm.js +2201 -0
  26. package/dist/token-list-bridge-utils.esm.js.map +1 -0
  27. package/dist/utils.d.ts +4 -0
  28. package/dist/verify.d.ts +9 -0
  29. package/package.json +68 -0
  30. package/src/arb-token-lists/.env.sample +4 -0
  31. package/src/arb-token-lists/.eslintrc.js +43 -0
  32. package/src/arb-token-lists/.gitignore +4 -0
  33. package/src/arb-token-lists/package.json +40 -0
  34. package/src/arb-token-lists/readme.md +14 -0
  35. package/src/arb-token-lists/server/app.ts +21 -0
  36. package/src/arb-token-lists/src/Assets/coingecko_uris.json +5108 -0
  37. package/src/arb-token-lists/src/Assets/logo_uris.json +44 -0
  38. package/src/arb-token-lists/src/WarningList/warningTokens.json +290 -0
  39. package/src/arb-token-lists/src/lib/getClargs.ts +32 -0
  40. package/src/arb-token-lists/src/lib/graph.ts +116 -0
  41. package/src/arb-token-lists/src/lib/instantiate_bridge.ts +35 -0
  42. package/src/arb-token-lists/src/lib/token_list_gen.ts +334 -0
  43. package/src/arb-token-lists/src/lib/types.ts +44 -0
  44. package/src/arb-token-lists/src/lib/utils.ts +285 -0
  45. package/src/arb-token-lists/src/main.ts +54 -0
  46. package/src/arb-token-lists/tsconfig.json +20 -0
  47. package/src/arb-token-lists/update_all +6 -0
  48. package/src/arb-token-lists/yarn.lock +4273 -0
  49. package/src/constants/chainId.ts +13 -0
  50. package/src/constants/tokens.ts +31 -0
  51. package/src/fixtures/index.ts +104 -0
  52. package/src/index.test.ts +211 -0
  53. package/src/index.ts +92 -0
  54. package/src/providers/ArbitrumMappingProvider.ts +28 -0
  55. package/src/providers/MappingProvider.ts +5 -0
  56. package/src/providers/OptimismMappingProvider.ts +13 -0
  57. package/src/providers/PolygonMappingProvider.ts +127 -0
  58. package/src/providers/index.ts +17 -0
  59. package/src/utils.ts +21 -0
  60. package/src/verify.test.ts +19 -0
  61. package/src/verify.ts +60 -0
@@ -0,0 +1,334 @@
1
+ import { minVersionBump, nextVersion, TokenList, VersionUpgrade } from '@uniswap/token-lists';
2
+ import { constants as arbConstants } from "arb-ts";
3
+ import { constants, utils } from 'ethers';
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { getAllTokens, getTokens } from './graph';
6
+ import { getNetworkConfig } from './instantiate_bridge';
7
+ import { ArbTokenInfo, ArbTokenList, EtherscanList, GraphTokenResult } from './types';
8
+ import {
9
+ getL2TokenAddressesFromL1,
10
+ getL2TokenAddressesFromL2,
11
+ getLogoUri,
12
+ getTokenListObj, isArbTokenList, listNameToArbifiedListName, listNameToFileName, removeInvalidTokensFromList, sanitizeString, validateTokenListWithErrorThrowing
13
+ } from './utils';
14
+
15
+
16
+ export interface ArbificationOptions {
17
+ overwriteCurrentList: boolean;
18
+ }
19
+
20
+ export interface L2ToL1GatewayAddresses {
21
+ [contractAddress: string]: string;
22
+ }
23
+
24
+ // TODO: read these values from the gateway or a subgraph
25
+ const l2ToL1GatewayAddresses: L2ToL1GatewayAddresses = {
26
+ // L2 ERC20 Gateway mainnet
27
+ '0x09e9222e96e7b4ae2a407b98d48e330053351eee':
28
+ '0xa3A7B6F88361F48403514059F1F16C8E78d60EeC',
29
+ // L2 Arb-Custom Gateway mainnet
30
+ '0x096760f208390250649e3e8763348e783aef5562':
31
+ '0xcEe284F754E854890e311e3280b767F80797180d',
32
+ // L2 weth mainnet
33
+ '0x6c411ad3e74de3e7bd422b94a27770f5b86c623b':
34
+ '0xd92023E9d9911199a6711321D1277285e6d4e2db',
35
+ // L2 dai gateway mainnet
36
+ '0x467194771dae2967aef3ecbedd3bf9a310c76c65':
37
+ '0xd3b5b60020504bc3489d6949d545893982ba3011',
38
+ // L2 ERC20 Gateway rinkeby
39
+ "0x195c107f3f75c4c93eba7d9a1312f19305d6375f": "0x91169Dbb45e6804743F94609De50D511C437572E",
40
+ // L2 Arb-Custom Gateway rinkeby
41
+ "0x9b014455acc2fe90c52803849d0002aeec184a06":"0x917dc9a69F65dC3082D518192cd3725E1Fa96cA2",
42
+ // L2 Weth Gateway rinkeby
43
+ "0xf94bc045c4e926cc0b34e8d1c41cd7a043304ac9": "0x81d1a19cf7071732D4313c75dE8DD5b8CF697eFD",
44
+ // old L2 weth gateway in rinkeby? we can prob remove this
45
+ "0xf90eb31045d5b924900aff29344deb42eae0b087": "0x81d1a19cf7071732D4313c75dE8DD5b8CF697eFD",
46
+ // livepeer gateway mainnet
47
+ "0x6d2457a4ad276000a615295f7a80f79e48ccd318": "0x6142f1C8bBF02E6A6bd074E8d564c9A5420a0676"
48
+ };
49
+
50
+ export const generateTokenList = async (
51
+ l1TokenList: TokenList,
52
+ prevArbTokenList?: ArbTokenList,
53
+ options?: {
54
+ /**
55
+ * Append all tokens from the original l1TokenList to the output list.
56
+ */
57
+ includeAllL1Tokens?: boolean,
58
+ /**
59
+ * Append all unbridged tokens from original l1TokenList to the output list.
60
+ */
61
+ includeUnbridgedL1Tokens?: boolean,
62
+ getAllTokensInNetwork?: boolean
63
+ }
64
+ ) => {
65
+ if(options?.includeAllL1Tokens && options.includeUnbridgedL1Tokens) {
66
+ throw new Error("Cannot include both of AllL1Tokens and UnbridgedL1Tokens since UnbridgedL1Tokens is a subset of AllL1Tokens.")
67
+ }
68
+
69
+ const name = l1TokenList.name
70
+ const mainLogoUri = l1TokenList.logoURI
71
+
72
+ const { l1 , l2 } = await getNetworkConfig();
73
+
74
+ let tokens =
75
+ options && options.getAllTokensInNetwork
76
+ ? await getAllTokens(l2.network.chainID)
77
+ : await getTokens(
78
+ l1TokenList.tokens.map((token) => token.address.toLowerCase()),
79
+ l2.network.chainID
80
+ );
81
+
82
+
83
+ const l1TokenAddresses = tokens.map((token:GraphTokenResult) => token.l1TokenAddr);
84
+ const l2AddressesFromL1 = await getL2TokenAddressesFromL1(l1TokenAddresses, l1.multiCaller, l2.network.tokenBridge.l1GatewayRouter);
85
+ const l2AddressesFromL2 = await getL2TokenAddressesFromL2(l1TokenAddresses, l2.multiCaller, l2.network.tokenBridge.l2GatewayRouter);
86
+
87
+ // if the l2 route hasn't been updated yet we remove the token from the bridged tokens
88
+ tokens = tokens.filter((t, i) => l2AddressesFromL1[i] === l2AddressesFromL2[i])
89
+
90
+ const tokenData = await l2.multiCaller.getTokenData(
91
+ l2AddressesFromL1.map(t => t || constants.AddressZero),
92
+ { name: true, decimals: true, symbol: true }
93
+ )
94
+ const logoUris: (string | undefined)[] = [];
95
+ for (const token of tokens) {
96
+ const uri = await getLogoUri(token.l1TokenAddr);
97
+ logoUris.push(uri);
98
+ }
99
+
100
+ const arbifiedTokens: {arbTokenInfo: ArbTokenInfo, l1TokenInfo: ArbTokenInfo}[] = tokens
101
+ .map((t, i) => ({token: t, l2Address: l2AddressesFromL2[i], tokenDatum: tokenData[i]}))
102
+ // it's possible that even though l2AddressesFromL1[i] === l2AddressesFromL2[i] these addresses could be the zero address
103
+ // this can happen if the graphql query returns an address that hasnt been bridged
104
+ .filter((t): t is typeof t & { l2Address: string } => t.l2Address != undefined && t.l2Address !== constants.AddressZero)
105
+ .map((token, i: number) => {
106
+ const l2GatewayAddress = token.token.joinTableEntry[0].gateway.gatewayAddr;
107
+ let { name:_name, decimals, symbol:_symbol } = token.tokenDatum;
108
+
109
+ if(decimals === undefined) throw new Error(`Unexpected undefined token decimals: ${JSON.stringify(token)}`);
110
+
111
+ _name = (() => {
112
+ if(_name === undefined) throw new Error(`Unexpected undefined token name: ${JSON.stringify(token)}`);
113
+ // if token name is empty, instead set the address as the name
114
+ // we remove the initial 0x since the token list standard only allows up to 40 characters
115
+ else if(_name === "") return token.token.l1TokenAddr.substring(2)
116
+ // parse null terminated bytes32 strings
117
+ else if(_name.length === 64) return utils.parseBytes32String("0x" + _name)
118
+ else return _name;
119
+ })()
120
+
121
+ _symbol = (() => {
122
+ if(_symbol === undefined) throw new Error(`Unexpected undefined token symbol: ${JSON.stringify(token)}`);
123
+ // schema doesn't allow for empty symbols, and has a max length of 20
124
+ else if (_symbol === "") return _name.substring(0, Math.min(_name.length, 20));
125
+ // parse null terminated bytes32 strings
126
+ else if (_symbol.length === 64) return utils.parseBytes32String("0x" + _symbol);
127
+ else return _symbol;
128
+ })()
129
+
130
+ const name = sanitizeString(_name)
131
+ const symbol = sanitizeString(_symbol)
132
+
133
+ let arbTokenInfo:ArbTokenInfo = {
134
+ chainId: +l2.network.chainID,
135
+ address: token.l2Address,
136
+ name,
137
+ symbol,
138
+ decimals,
139
+ extensions: {
140
+ bridgeInfo: {
141
+ [l1.network.chainID]: {
142
+ tokenAddress: token.token.l1TokenAddr,
143
+ originBridgeAddress: l2GatewayAddress,
144
+ destBridgeAddress: l2ToL1GatewayAddresses[l2GatewayAddress.toLowerCase()]
145
+ }
146
+ }
147
+ }
148
+ };
149
+
150
+ const l1TokenInfo:ArbTokenInfo = {
151
+ chainId: +l1.network.chainID,
152
+ address: token.token.l1TokenAddr,
153
+ name,symbol,decimals,
154
+ extensions: {
155
+ bridgeInfo: {
156
+ [l2.network.chainID]: {
157
+ tokenAddress: token.l2Address,
158
+ originBridgeAddress: l2ToL1GatewayAddresses[l2GatewayAddress.toLowerCase()],
159
+ destBridgeAddress: l2GatewayAddress
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ if (logoUris[i]) {
166
+ arbTokenInfo = { ...{ logoURI: logoUris[i] }, ...arbTokenInfo };
167
+ } else {
168
+ console.log('no logo uri for ',token.token.l1TokenAddr, symbol);
169
+
170
+ }
171
+
172
+ return { arbTokenInfo, l1TokenInfo };
173
+ }).filter(({arbTokenInfo}: {arbTokenInfo: ArbTokenInfo})=>{
174
+ return arbTokenInfo.extensions && arbTokenInfo.extensions.bridgeInfo[l1.network.chainID].originBridgeAddress !== arbConstants.DISABLED_GATEWAY
175
+ })
176
+
177
+ let arbifiedTokenList = arbifiedTokens
178
+ .map(({arbTokenInfo}) => arbTokenInfo)
179
+ .sort((a, b) => (a.symbol < b.symbol ? -1 : 1));
180
+ const l1ArbedTokenList = arbifiedTokens.map(({l1TokenInfo}) => l1TokenInfo)
181
+
182
+ console.log(`List has ${arbifiedTokenList.length} bridged tokens`);
183
+
184
+ if(options?.includeAllL1Tokens) {
185
+ arbifiedTokenList = arbifiedTokenList.concat(l1ArbedTokenList)
186
+ } else if(options?.includeUnbridgedL1Tokens) {
187
+ const l1AddressesOfBridgedTokens = new Set(tokens.map((token)=> token.l1TokenAddr.toLowerCase()))
188
+ const unbridgedTokens = l1ArbedTokenList.filter((l1TokenInfo)=>{
189
+ return !l1AddressesOfBridgedTokens.has(l1TokenInfo.address.toLowerCase()) && l1TokenInfo.chainId === +l1.network.chainID
190
+ }).sort((a, b) => (a.symbol < b.symbol ? -1 : 1))
191
+ console.log(`List has ${unbridgedTokens.length} unbridged tokens`);
192
+
193
+ arbifiedTokenList = arbifiedTokenList.concat(unbridgedTokens)
194
+ }
195
+
196
+ const version = (()=>{
197
+ if(prevArbTokenList){
198
+ // @ts-ignore
199
+ let versionBump = minVersionBump(prevArbTokenList.tokens, arbifiedTokenList)
200
+
201
+ // tmp: library doesn't nicely handle patches (for extensions object)
202
+ if(versionBump === VersionUpgrade.PATCH){
203
+ versionBump = VersionUpgrade.NONE
204
+ }
205
+ return nextVersion(prevArbTokenList.version, versionBump)
206
+ }
207
+ return {
208
+ major: 1,
209
+ minor: 0,
210
+ patch: 0,
211
+ }
212
+ })()
213
+
214
+ const arbTokenList: ArbTokenList = {
215
+ name: listNameToArbifiedListName(name),
216
+ timestamp: new Date().toISOString(),
217
+ version,
218
+ tokens: arbifiedTokenList,
219
+ logoURI: mainLogoUri
220
+ };
221
+
222
+ const validationTokenList: ArbTokenList = {
223
+ ...arbTokenList,
224
+ tokens: arbTokenList.tokens
225
+ };
226
+ validateTokenListWithErrorThrowing(validationTokenList);
227
+
228
+ console.log(`Generated list with total ${arbTokenList.tokens.length} tokens`);
229
+ console.log('version:', version);
230
+
231
+ return arbTokenList;
232
+ };
233
+
234
+ export const arbifyL1List = async (pathOrUrl: string) => {
235
+ const l1TokenList = await getTokenListObj(pathOrUrl);
236
+ removeInvalidTokensFromList(l1TokenList)
237
+ const path = process.env.PWD +
238
+ '/src/ArbTokenLists/' +
239
+ listNameToFileName(l1TokenList.name);
240
+ let prevArbTokenList: ArbTokenList | undefined;
241
+
242
+ if(existsSync(path)){
243
+ const data = readFileSync(path)
244
+ console.log('Prev version of Arb List found');
245
+
246
+ prevArbTokenList = JSON.parse(data.toString()) as ArbTokenList
247
+ isArbTokenList(prevArbTokenList)
248
+ }
249
+
250
+ const l1Addresses = l1TokenList.tokens.map((token) =>
251
+ token.address.toLowerCase()
252
+ );
253
+
254
+ const newList = await generateTokenList(l1TokenList, prevArbTokenList, {
255
+ includeAllL1Tokens: true
256
+ });
257
+
258
+ writeFileSync(path, JSON.stringify(newList));
259
+ console.log('Token list generated at', path );
260
+
261
+ };
262
+
263
+ export const updateArbifiedList = async (pathOrUrl: string) => {
264
+ const arbTokenList = await getTokenListObj(pathOrUrl);
265
+ removeInvalidTokensFromList(arbTokenList)
266
+ const path = process.env.PWD +
267
+ '/src/ArbTokenLists/' +
268
+ listNameToFileName(arbTokenList.name);
269
+ let prevArbTokenList: ArbTokenList | undefined;
270
+
271
+ if(existsSync(path)){
272
+ const data = readFileSync(path)
273
+ console.log('Prev version of Arb List found');
274
+
275
+ prevArbTokenList = JSON.parse(data.toString()) as ArbTokenList
276
+ isArbTokenList(prevArbTokenList)
277
+ }
278
+
279
+ const newList = await generateTokenList(arbTokenList, prevArbTokenList, {
280
+ includeAllL1Tokens: true
281
+ });
282
+
283
+ writeFileSync(path, JSON.stringify(newList));
284
+ console.log('Token list generated at', path );
285
+
286
+ };
287
+
288
+
289
+ export const updateLogoURIs = async (path: string)=> {
290
+ const data = readFileSync(path)
291
+ const prevArbTokenList = JSON.parse(data.toString()) as ArbTokenList
292
+ const tokens:any = []
293
+ for (let i = 0; i < prevArbTokenList.tokens.length; i++) {
294
+ const tokenInfo = {...prevArbTokenList.tokens[i]}
295
+
296
+ // @ts-ignore
297
+ const logoURI = await getLogoUri(tokenInfo.extensions.l1Address)
298
+ if(logoURI){
299
+ tokenInfo.logoURI = logoURI
300
+ } else {
301
+ console.log('not found:', tokenInfo);
302
+ delete tokenInfo.logoURI
303
+ }
304
+ tokens.push(tokenInfo)
305
+ }
306
+
307
+ const newArbList = {...prevArbTokenList, ...{tokens: tokens}}
308
+ writeFileSync(path, JSON.stringify(newArbList));
309
+
310
+ }
311
+
312
+ export const arbListtoEtherscanList = (
313
+ arbList: ArbTokenList
314
+ ): EtherscanList => {
315
+ const list: EtherscanList = [];
316
+ arbList.tokens.forEach(tokenInfo => {
317
+ const { address: l2Address } = tokenInfo;
318
+ if (tokenInfo.extensions) {
319
+ // This assumes one origin chain; should be chill
320
+ const originChainID = Object.keys(tokenInfo.extensions.bridgeInfo)[0];
321
+ const { tokenAddress, originBridgeAddress, destBridgeAddress } =
322
+ tokenInfo.extensions.bridgeInfo[originChainID];
323
+ const data = {
324
+ l1Address: tokenAddress,
325
+ l2Address,
326
+ l1GatewayAddress: destBridgeAddress,
327
+ l2GatewayAddress: originBridgeAddress
328
+ };
329
+ list.push(data);
330
+ }
331
+ });
332
+ return list;
333
+ };
334
+
@@ -0,0 +1,44 @@
1
+ import { TokenInfo, TokenList } from '@uniswap/token-lists';
2
+
3
+ // extensions object is allowed to be 2 levels deep, but type is out-of-date
4
+ // https://github.com/Uniswap/token-lists/pull/67
5
+ export interface ArbTokenInfo extends Omit<TokenInfo, 'extensions'> {
6
+ extensions?: {
7
+ bridgeInfo: {
8
+ [destinationChainID: string]:{
9
+ tokenAddress: string,
10
+ originBridgeAddress: string,
11
+ destBridgeAddress: string
12
+ }
13
+ }
14
+ }
15
+ }
16
+
17
+ export interface ArbTokenList extends Omit<TokenList, 'tokens'> {
18
+ tokens: ArbTokenInfo[];
19
+ }
20
+
21
+
22
+ export interface EtherscanToken {
23
+ l1Address: string | null,
24
+ l2Address: string,
25
+ l1GatewayAddress: string | null,
26
+ l2GatewayAddress: string | null
27
+ }
28
+
29
+ export type EtherscanList = EtherscanToken[]
30
+
31
+
32
+ export interface GraphTokenResult {
33
+ l2Address: string | null,
34
+ joinTableEntry: [{
35
+ gateway: {
36
+ gatewayAddr: string
37
+ }
38
+ }],
39
+ l1TokenAddr: string
40
+ }
41
+
42
+ export interface GraphTokensResult {
43
+ tokens: GraphTokenResult[]
44
+ }
@@ -0,0 +1,285 @@
1
+ import { schema, TokenList } from '@uniswap/token-lists'
2
+ import Ajv from 'ajv'
3
+ import addFormats from 'ajv-formats'
4
+ import { abi, MultiCaller } from 'arb-ts'
5
+ import axios from 'axios'
6
+ import betterAjvErrors from 'better-ajv-errors'
7
+ import { existsSync, readFileSync } from 'fs'
8
+ import path from 'path'
9
+ import { ArbTokenList } from './types'
10
+
11
+ const coinGeckoBuff = readFileSync(path.resolve(__dirname, '../Assets/coingecko_uris.json'))
12
+ const logoURIsBuff = readFileSync(path.resolve(__dirname, '../Assets/logo_uris.json'))
13
+
14
+ const coingeckoURIs = JSON.parse(coinGeckoBuff.toString())
15
+ const logoUris = JSON.parse(logoURIsBuff.toString())
16
+ for (const address of Object.keys(logoUris)) {
17
+ logoUris[address.toLowerCase()] = logoUris[address]
18
+ }
19
+
20
+ export const listNameToFileName = (name: string) => {
21
+ const prefix = 'arbed_'
22
+ let fileName = name.split(' ').join('_').toLowerCase() + '.json'
23
+ if (!fileName.startsWith(prefix)) {
24
+ fileName = prefix + fileName
25
+ }
26
+ return fileName
27
+ }
28
+
29
+ export const listNameToArbifiedListName = (name: string) => {
30
+ const prefix = 'Arbed '
31
+
32
+ let fileName = sanitizeString(name)
33
+ if (!fileName.startsWith(prefix)) {
34
+ fileName = prefix + fileName
35
+ }
36
+ return fileName.split(' ').slice(0, 2).join(' ').slice(0, 20)
37
+ }
38
+
39
+ export const getL2TokenAddressesFromL1 = async (
40
+ l1TokenAddresses: string[],
41
+ multiCaller: MultiCaller,
42
+ l1GatewayRouterAddress: string,
43
+ ) => {
44
+ const iFace = abi.L1GatewayRouter__factory.createInterface()
45
+
46
+ return await multiCaller.multiCall(
47
+ l1TokenAddresses.map((addr) => ({
48
+ encoder: () =>
49
+ iFace.encodeFunctionData('calculateL2TokenAddress', [addr]),
50
+ decoder: (returnData: string) =>
51
+ iFace.decodeFunctionResult(
52
+ 'calculateL2TokenAddress',
53
+ returnData,
54
+ )[0] as string,
55
+ targetAddr: l1GatewayRouterAddress,
56
+ })),
57
+ )
58
+ }
59
+
60
+ export const getL2TokenAddressesFromL2 = async (
61
+ l1TokenAddresses: string[],
62
+ multiCaller: MultiCaller,
63
+ l2GatewayRouterAddress: string,
64
+ ) => {
65
+ const iFace = abi.L2GatewayRouter__factory.createInterface()
66
+
67
+ return await multiCaller.multiCall(
68
+ l1TokenAddresses.map((addr) => ({
69
+ encoder: () =>
70
+ iFace.encodeFunctionData('calculateL2TokenAddress', [addr]),
71
+ decoder: (returnData: string) =>
72
+ iFace.decodeFunctionResult(
73
+ 'calculateL2TokenAddress',
74
+ returnData,
75
+ )[0] as string,
76
+ targetAddr: l2GatewayRouterAddress,
77
+ })),
78
+ )
79
+ }
80
+
81
+ export const getLogoUri = async (l1TokenAddress: string) => {
82
+ const l1TokenAddressLCase = l1TokenAddress.toLowerCase()
83
+ const logoUri: string | undefined = logoUris[l1TokenAddressLCase]
84
+ const coinGeckoURI: string | undefined = coingeckoURIs[l1TokenAddressLCase]
85
+ const trustWalletUri = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${l1TokenAddress}/logo.png`
86
+ const uris = [logoUri, coinGeckoURI, trustWalletUri].filter(
87
+ (x): x is string => !!x,
88
+ )
89
+
90
+ for (const uri of uris) {
91
+ try {
92
+ const res = await axios.get(uri)
93
+ if (res.status === 200) {
94
+ return uri
95
+ }
96
+ } catch (e) {}
97
+ }
98
+ return
99
+ }
100
+ export const getTokenListObjFromUrl = async (url: string) => {
101
+ return (await axios.get(url)).data as TokenList
102
+ }
103
+ export const getTokenListObjFromLocalPath = async (path: string) => {
104
+ return JSON.parse(readFileSync(path).toString()) as TokenList
105
+ }
106
+
107
+ export const tokenListIsValid = (tokenList: ArbTokenList | TokenList) => {
108
+ const ajv = new Ajv()
109
+ addFormats(ajv)
110
+ const validate = ajv.compile(schema)
111
+
112
+ const res = validate(tokenList)
113
+ if (validate.errors) {
114
+ const output = betterAjvErrors(schema, tokenList, validate.errors, {
115
+ indent: 2,
116
+ })
117
+ console.log(output)
118
+ }
119
+
120
+ return res
121
+ }
122
+
123
+ export const validateTokenListWithErrorThrowing = (
124
+ tokenList: ArbTokenList | TokenList,
125
+ ) => {
126
+ try {
127
+ const valid = tokenListIsValid(tokenList)
128
+ if (valid) return true
129
+ else throw new Error('Data does not conform to token list schema; not sure why')
130
+ } catch(e) {
131
+ console.log('Invalid token list:')
132
+ throw e
133
+ }
134
+ }
135
+
136
+ export const removeInvalidTokensFromList = (tokenList: ArbTokenList | TokenList):ArbTokenList | TokenList =>{
137
+ let valid = tokenListIsValid(tokenList);
138
+ const startingTokenListLen = tokenList.tokens.length
139
+
140
+ if (valid) {
141
+ return tokenList;
142
+ } else {
143
+ const tokenListCopy = JSON.parse(JSON.stringify(tokenList)) as typeof tokenList
144
+ console.log("Invalid token list:");
145
+ while (!valid && tokenListCopy.tokens.length > 0){
146
+ const targetToken = tokenListCopy.tokens.pop()
147
+ const tokenTokenIndex = tokenListCopy.tokens.length
148
+ valid = tokenListIsValid(tokenListCopy);
149
+ if(valid){
150
+ console.log('Invalid token token, removing from list', targetToken);
151
+
152
+ tokenList.tokens.splice(tokenTokenIndex , 1)
153
+ // pre-recursion sanity check:
154
+ if( tokenList.tokens.length >= startingTokenListLen ){
155
+ throw new Error("666: removeInvalidTokensFromList failed basic sanity check")
156
+ }
157
+ return removeInvalidTokensFromList(tokenList)
158
+ }
159
+ }
160
+ throw new Error('Data does not confirm to token list schema; not sure why');
161
+ }
162
+ }
163
+
164
+ export const getTokenListObj = async (pathOrUrl: string) => {
165
+ const tokenList: TokenList = await (async (pathOrUrl: string) => {
166
+ const localFileExists = existsSync(pathOrUrl)
167
+ const looksLikeUrl = isValidHttpUrl(pathOrUrl)
168
+ if (localFileExists) {
169
+ return getTokenListObjFromLocalPath(pathOrUrl)
170
+ } else if (looksLikeUrl) {
171
+ return await getTokenListObjFromUrl(pathOrUrl)
172
+ } else {
173
+ throw new Error('Could not find token list')
174
+ }
175
+ })(pathOrUrl);
176
+ isTokenList(tokenList)
177
+ return tokenList
178
+ };
179
+
180
+ // https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
181
+
182
+ function isValidHttpUrl(urlString: string) {
183
+ let url
184
+
185
+ try {
186
+ url = new URL(urlString)
187
+ } catch (_) {
188
+ return false
189
+ }
190
+
191
+ return url.protocol === 'http:' || url.protocol === 'https:'
192
+ }
193
+
194
+ // typeguard:
195
+ export const isArbTokenList = (obj: any) => {
196
+ const expectedListKeys = ['name', 'timestamp', 'version', 'tokens']
197
+ const actualListKeys = new Set(Object.keys(obj))
198
+ if (!expectedListKeys.every((key) => actualListKeys.has(key))) {
199
+ throw new Error(
200
+ 'ArbTokenList typeguard error: requried list key not included',
201
+ )
202
+ }
203
+ const { version, tokens } = obj
204
+ if (
205
+ !['major', 'minor', 'patch'].every((key) => {
206
+ return typeof version[key] === 'number'
207
+ })
208
+ ) {
209
+ throw new Error('ArbTokenList typeguard error: invalid version')
210
+ }
211
+ if (
212
+ !tokens.every((token: any) => {
213
+ const tokenKeys = new Set(Object.keys(token))
214
+ return ['chainId', 'address', 'name', 'decimals', 'symbol'].every(
215
+ (key) => {
216
+ return tokenKeys.has(key)
217
+ },
218
+ )
219
+ })
220
+ ) {
221
+ throw new Error('ArbTokenList typeguard error: token missing required key')
222
+ }
223
+ tokens.forEach((token: any) => {
224
+ if (token.extensions && token.extensions.bridgeInfo) {
225
+ const {
226
+ extensions: { bridgeInfo },
227
+ } = token
228
+ const bridges = Object.keys(bridgeInfo)
229
+ if (!bridges.length) {
230
+ throw new Error('ArbTokenList typeguard error: no bridge info found')
231
+ }
232
+ const someDestinationChain = bridges[0]
233
+ const {
234
+ tokenAddress,
235
+ originBridgeAddress,
236
+ destBridgeAddress,
237
+ } = bridgeInfo[someDestinationChain]
238
+
239
+ if (
240
+ ![tokenAddress, originBridgeAddress, destBridgeAddress].every((k) => k)
241
+ ) {
242
+ throw new Error('ArbTokenList typeguard error: missing extension')
243
+ }
244
+ }
245
+ })
246
+ }
247
+
248
+
249
+ // typeguard:
250
+ export const isTokenList = (obj:any)=>{
251
+ const expectedListKeys = ['name', 'timestamp', 'version', 'tokens']
252
+ const actualListKeys = new Set(Object.keys(obj))
253
+ if(!expectedListKeys.every((key)=>actualListKeys.has(key) )){
254
+ throw new Error("tokenlist typeguard error: requried list key not included")
255
+ }
256
+ const { version, tokens } = obj
257
+ if(!['major','minor', 'patch'].every((key)=>{
258
+ return typeof version[key] === 'number'
259
+ })){
260
+ throw new Error("tokenlist typeguard error: invalid version")
261
+ }
262
+ if(!tokens.every((token:any)=>{
263
+ const tokenKeys = new Set(Object.keys(token))
264
+ return ['chainId', 'address','name', 'decimals','symbol' ].every((key)=>{
265
+ return tokenKeys.has(key)
266
+ })
267
+ })){
268
+ throw new Error ("tokenlist typeguard error: token missing required key")
269
+ }
270
+ }
271
+
272
+ export const sanitizeString = (str: string) =>
273
+ str.replace(/[^ \w.'+\-%/À-ÖØ-öø-ÿ:&\[\]\(\)]/gi, '')
274
+
275
+ export const excludeList = [
276
+ '0x0CE51000d5244F1EAac0B313a792D5a5f96931BF', //rkr
277
+ '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f', //in
278
+ '0xEDA6eFE5556e134Ef52f2F858aa1e81c84CDA84b', // bad cap
279
+ '0xe54942077Df7b8EEf8D4e6bCe2f7B58B0082b0cd', // swapr
280
+ '0x282db609e787a132391eb64820ba6129fceb2695', // amy
281
+ '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', // mim
282
+ '0x106538cc16f938776c7c180186975bca23875287', // remove once bridged (basv2)
283
+ '0xB4A3B0Faf0Ab53df58001804DdA5Bfc6a3D59008', // spera
284
+ // "0x960b236a07cf122663c4303350609a66a7b288c0", //aragon old
285
+ ].map((s) => s.toLowerCase())