@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.
- package/LICENSE +674 -0
- package/README.md +17 -0
- package/dist/arb-token-lists/server/app.d.ts +1 -0
- package/dist/arb-token-lists/src/lib/getClargs.d.ts +8 -0
- package/dist/arb-token-lists/src/lib/graph.d.ts +2 -0
- package/dist/arb-token-lists/src/lib/instantiate_bridge.d.ts +15 -0
- package/dist/arb-token-lists/src/lib/token_list_gen.d.ts +23 -0
- package/dist/arb-token-lists/src/lib/types.d.ts +36 -0
- package/dist/arb-token-lists/src/lib/utils.d.ts +18 -0
- package/dist/arb-token-lists/src/main.d.ts +1 -0
- package/dist/constants/chainId.d.ts +10 -0
- package/dist/constants/tokens.d.ts +5 -0
- package/dist/fixtures/index.d.ts +24 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/providers/ArbitrumMappingProvider.d.ts +11 -0
- package/dist/providers/MappingProvider.d.ts +4 -0
- package/dist/providers/OptimismMappingProvider.d.ts +10 -0
- package/dist/providers/PolygonMappingProvider.d.ts +9 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/token-list-bridge-utils.cjs.development.js +2217 -0
- package/dist/token-list-bridge-utils.cjs.development.js.map +1 -0
- package/dist/token-list-bridge-utils.cjs.production.min.js +2 -0
- package/dist/token-list-bridge-utils.cjs.production.min.js.map +1 -0
- package/dist/token-list-bridge-utils.esm.js +2201 -0
- package/dist/token-list-bridge-utils.esm.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/verify.d.ts +9 -0
- package/package.json +68 -0
- package/src/arb-token-lists/.env.sample +4 -0
- package/src/arb-token-lists/.eslintrc.js +43 -0
- package/src/arb-token-lists/.gitignore +4 -0
- package/src/arb-token-lists/package.json +40 -0
- package/src/arb-token-lists/readme.md +14 -0
- package/src/arb-token-lists/server/app.ts +21 -0
- package/src/arb-token-lists/src/Assets/coingecko_uris.json +5108 -0
- package/src/arb-token-lists/src/Assets/logo_uris.json +44 -0
- package/src/arb-token-lists/src/WarningList/warningTokens.json +290 -0
- package/src/arb-token-lists/src/lib/getClargs.ts +32 -0
- package/src/arb-token-lists/src/lib/graph.ts +116 -0
- package/src/arb-token-lists/src/lib/instantiate_bridge.ts +35 -0
- package/src/arb-token-lists/src/lib/token_list_gen.ts +334 -0
- package/src/arb-token-lists/src/lib/types.ts +44 -0
- package/src/arb-token-lists/src/lib/utils.ts +285 -0
- package/src/arb-token-lists/src/main.ts +54 -0
- package/src/arb-token-lists/tsconfig.json +20 -0
- package/src/arb-token-lists/update_all +6 -0
- package/src/arb-token-lists/yarn.lock +4273 -0
- package/src/constants/chainId.ts +13 -0
- package/src/constants/tokens.ts +31 -0
- package/src/fixtures/index.ts +104 -0
- package/src/index.test.ts +211 -0
- package/src/index.ts +92 -0
- package/src/providers/ArbitrumMappingProvider.ts +28 -0
- package/src/providers/MappingProvider.ts +5 -0
- package/src/providers/OptimismMappingProvider.ts +13 -0
- package/src/providers/PolygonMappingProvider.ts +127 -0
- package/src/providers/index.ts +17 -0
- package/src/utils.ts +21 -0
- package/src/verify.test.ts +19 -0
- 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())
|