@pancakeswap/token-lists 0.0.2

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,186 @@
1
+ import { createReducer } from '@reduxjs/toolkit'
2
+
3
+ import {
4
+ acceptListUpdate,
5
+ addList,
6
+ fetchTokenList,
7
+ removeList,
8
+ enableList,
9
+ disableList,
10
+ updateListVersion,
11
+ } from './actions'
12
+ import { getVersionUpgrade, VersionUpgrade, TokenList } from '../src'
13
+
14
+ export interface ListsState {
15
+ readonly byUrl: {
16
+ readonly [url: string]: {
17
+ readonly current: TokenList | null
18
+ readonly pendingUpdate: TokenList | null
19
+ readonly loadingRequestId: string | null
20
+ readonly error: string | null
21
+ }
22
+ }
23
+ // this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
24
+ readonly lastInitializedDefaultListOfLists?: string[]
25
+
26
+ // currently active lists
27
+ readonly activeListUrls: string[] | undefined
28
+ }
29
+
30
+ type ListByUrlState = ListsState['byUrl'][string]
31
+
32
+ export const NEW_LIST_STATE: ListByUrlState = {
33
+ error: null,
34
+ current: null,
35
+ loadingRequestId: null,
36
+ pendingUpdate: null,
37
+ }
38
+
39
+ export const createTokenListReducer = (
40
+ initialState: ListsState,
41
+ DEFAULT_LIST_OF_LISTS: string[],
42
+ DEFAULT_ACTIVE_LIST_URLS: string[],
43
+ ) =>
44
+ createReducer(initialState, (builder) =>
45
+ builder
46
+ .addCase(fetchTokenList.pending, (state, { payload: { requestId, url } }) => {
47
+ const current = state.byUrl[url]?.current ?? null
48
+ const pendingUpdate = state.byUrl[url]?.pendingUpdate ?? null
49
+
50
+ state.byUrl[url] = {
51
+ current,
52
+ pendingUpdate,
53
+ loadingRequestId: requestId,
54
+ error: null,
55
+ }
56
+ })
57
+ .addCase(fetchTokenList.fulfilled, (state, { payload: { requestId, tokenList, url } }) => {
58
+ const current = state.byUrl[url]?.current
59
+ const loadingRequestId = state.byUrl[url]?.loadingRequestId
60
+
61
+ // no-op if update does nothing
62
+ if (current) {
63
+ const upgradeType = getVersionUpgrade(current.version, tokenList.version)
64
+
65
+ if (upgradeType === VersionUpgrade.NONE) return
66
+ if (loadingRequestId === null || loadingRequestId === requestId) {
67
+ state.byUrl[url] = {
68
+ ...state.byUrl[url],
69
+ loadingRequestId: null,
70
+ error: null,
71
+ current,
72
+ pendingUpdate: tokenList,
73
+ }
74
+ }
75
+ } else {
76
+ // activate if on default active
77
+ if (DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
78
+ state.activeListUrls?.push(url)
79
+ }
80
+
81
+ state.byUrl[url] = {
82
+ ...state.byUrl[url],
83
+ loadingRequestId: null,
84
+ error: null,
85
+ current: tokenList,
86
+ pendingUpdate: null,
87
+ }
88
+ }
89
+ })
90
+ .addCase(fetchTokenList.rejected, (state, { payload: { url, requestId, errorMessage } }) => {
91
+ if (state.byUrl[url]?.loadingRequestId !== requestId) {
92
+ // no-op since it's not the latest request
93
+ return
94
+ }
95
+
96
+ state.byUrl[url] = {
97
+ ...state.byUrl[url],
98
+ loadingRequestId: null,
99
+ error: errorMessage,
100
+ current: null,
101
+ pendingUpdate: null,
102
+ }
103
+ })
104
+ .addCase(addList, (state, { payload: url }) => {
105
+ if (!state.byUrl[url]) {
106
+ state.byUrl[url] = NEW_LIST_STATE
107
+ }
108
+ })
109
+ .addCase(removeList, (state, { payload: url }) => {
110
+ if (state.byUrl[url]) {
111
+ delete state.byUrl[url]
112
+ }
113
+ // remove list from active urls if needed
114
+ if (state.activeListUrls && state.activeListUrls.includes(url)) {
115
+ state.activeListUrls = state.activeListUrls.filter((u) => u !== url)
116
+ }
117
+ })
118
+ .addCase(enableList, (state, { payload: url }) => {
119
+ if (!state.byUrl[url]) {
120
+ state.byUrl[url] = NEW_LIST_STATE
121
+ }
122
+
123
+ if (state.activeListUrls && !state.activeListUrls.includes(url)) {
124
+ state.activeListUrls.push(url)
125
+ }
126
+
127
+ if (!state.activeListUrls) {
128
+ state.activeListUrls = [url]
129
+ }
130
+ })
131
+ .addCase(disableList, (state, { payload: url }) => {
132
+ if (state.activeListUrls && state.activeListUrls.includes(url)) {
133
+ state.activeListUrls = state.activeListUrls.filter((u) => u !== url)
134
+ }
135
+ })
136
+ .addCase(acceptListUpdate, (state, { payload: url }) => {
137
+ if (!state.byUrl[url]?.pendingUpdate) {
138
+ throw new Error('accept list update called without pending update')
139
+ }
140
+ state.byUrl[url] = {
141
+ ...state.byUrl[url],
142
+ pendingUpdate: null,
143
+ current: state.byUrl[url].pendingUpdate,
144
+ }
145
+ })
146
+ .addCase(updateListVersion, (state) => {
147
+ // state loaded from localStorage, but new lists have never been initialized
148
+ if (!state.lastInitializedDefaultListOfLists) {
149
+ state.byUrl = initialState.byUrl
150
+ state.activeListUrls = initialState.activeListUrls
151
+ } else if (state.lastInitializedDefaultListOfLists) {
152
+ const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
153
+ (s, l) => s.add(l),
154
+ new Set(),
155
+ )
156
+ const newListOfListsSet = DEFAULT_LIST_OF_LISTS.reduce<Set<string>>((s, l) => s.add(l), new Set())
157
+
158
+ DEFAULT_LIST_OF_LISTS.forEach((listUrl) => {
159
+ if (!lastInitializedSet.has(listUrl)) {
160
+ state.byUrl[listUrl] = NEW_LIST_STATE
161
+ }
162
+ })
163
+
164
+ state.lastInitializedDefaultListOfLists.forEach((listUrl) => {
165
+ if (!newListOfListsSet.has(listUrl)) {
166
+ delete state.byUrl[listUrl]
167
+ }
168
+ })
169
+ }
170
+
171
+ state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
172
+
173
+ // if no active lists, activate defaults
174
+ if (!state.activeListUrls) {
175
+ state.activeListUrls = DEFAULT_ACTIVE_LIST_URLS
176
+
177
+ // for each list on default list, initialize if needed
178
+ DEFAULT_ACTIVE_LIST_URLS.forEach((listUrl: string) => {
179
+ if (!state.byUrl[listUrl]) {
180
+ state.byUrl[listUrl] = NEW_LIST_STATE
181
+ }
182
+ return true
183
+ })
184
+ }
185
+ }),
186
+ )
@@ -0,0 +1,37 @@
1
+ import { nanoid } from '@reduxjs/toolkit'
2
+ import { useCallback } from 'react'
3
+ import { fetchTokenList } from './actions'
4
+ import { TokenList } from '../src/types'
5
+
6
+ function useFetchListCallback(
7
+ dispatch: (action?: unknown) => void,
8
+ ): (listUrl: string, sendDispatch?: boolean) => Promise<TokenList> {
9
+ // note: prevent dispatch if using for list search or unsupported list
10
+ return useCallback(
11
+ async (listUrl: string, sendDispatch = true) => {
12
+ const requestId = nanoid()
13
+ if (sendDispatch) {
14
+ dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
15
+ }
16
+ // lazy load avj and token list schema
17
+ const getTokenList = (await import('./getTokenList')).default
18
+ return getTokenList(listUrl)
19
+ .then((tokenList) => {
20
+ if (sendDispatch) {
21
+ dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
22
+ }
23
+ return tokenList
24
+ })
25
+ .catch((error) => {
26
+ console.error(`Failed to get list at url ${listUrl}`, error)
27
+ if (sendDispatch) {
28
+ dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
29
+ }
30
+ throw error
31
+ })
32
+ },
33
+ [dispatch],
34
+ )
35
+ }
36
+
37
+ export default useFetchListCallback
@@ -0,0 +1,392 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "pancakeswap",
4
+ "title": "PancakeSwap Token List",
5
+ "description": "Schema for lists of tokens compatible with the PancakeSwap Interface, including Uniswap standard and PancakeSwap Aptos",
6
+ "definitions": {
7
+ "Version": {
8
+ "type": "object",
9
+ "description": "The version of the list, used in change detection",
10
+ "examples": [
11
+ {
12
+ "major": 1,
13
+ "minor": 0,
14
+ "patch": 0
15
+ }
16
+ ],
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "major": {
20
+ "type": "integer",
21
+ "description": "The major version of the list. Must be incremented when tokens are removed from the list or token addresses are changed.",
22
+ "minimum": 0,
23
+ "examples": [1, 2]
24
+ },
25
+ "minor": {
26
+ "type": "integer",
27
+ "description": "The minor version of the list. Must be incremented when tokens are added to the list.",
28
+ "minimum": 0,
29
+ "examples": [0, 1]
30
+ },
31
+ "patch": {
32
+ "type": "integer",
33
+ "description": "The patch version of the list. Must be incremented for any changes to the list.",
34
+ "minimum": 0,
35
+ "examples": [0, 1]
36
+ }
37
+ },
38
+ "required": ["major", "minor", "patch"]
39
+ },
40
+ "TagIdentifier": {
41
+ "type": "string",
42
+ "description": "The unique identifier of a tag",
43
+ "minLength": 1,
44
+ "maxLength": 10,
45
+ "pattern": "^[\\w]+$",
46
+ "examples": ["compound", "stablecoin"]
47
+ },
48
+ "ExtensionIdentifier": {
49
+ "type": "string",
50
+ "description": "The name of a token extension property",
51
+ "minLength": 1,
52
+ "maxLength": 40,
53
+ "pattern": "^[\\w]+$",
54
+ "examples": ["color", "is_fee_on_transfer", "aliases"]
55
+ },
56
+ "ExtensionMap": {
57
+ "type": "object",
58
+ "description": "An object containing any arbitrary or vendor-specific token metadata",
59
+ "maxProperties": 10,
60
+ "propertyNames": {
61
+ "$ref": "#/definitions/ExtensionIdentifier"
62
+ },
63
+ "additionalProperties": {
64
+ "$ref": "#/definitions/ExtensionValue"
65
+ },
66
+ "examples": [
67
+ {
68
+ "color": "#000000",
69
+ "is_verified_by_me": true
70
+ },
71
+ {
72
+ "x-bridged-addresses-by-chain": {
73
+ "1": {
74
+ "bridgeAddress": "0x4200000000000000000000000000000000000010",
75
+ "tokenAddress": "0x4200000000000000000000000000000000000010"
76
+ }
77
+ }
78
+ }
79
+ ]
80
+ },
81
+ "ExtensionPrimitiveValue": {
82
+ "anyOf": [
83
+ {
84
+ "type": "string",
85
+ "minLength": 1,
86
+ "maxLength": 42,
87
+ "examples": ["#00000"]
88
+ },
89
+ {
90
+ "type": "boolean",
91
+ "examples": [true]
92
+ },
93
+ {
94
+ "type": "number",
95
+ "examples": [15]
96
+ },
97
+ {
98
+ "type": "null"
99
+ }
100
+ ]
101
+ },
102
+ "ExtensionValue": {
103
+ "anyOf": [
104
+ {
105
+ "$ref": "#/definitions/ExtensionPrimitiveValue"
106
+ },
107
+ {
108
+ "type": "object",
109
+ "maxProperties": 10,
110
+ "propertyNames": {
111
+ "$ref": "#/definitions/ExtensionIdentifier"
112
+ },
113
+ "additionalProperties": {
114
+ "$ref": "#/definitions/ExtensionValueInner0"
115
+ }
116
+ }
117
+ ]
118
+ },
119
+ "ExtensionValueInner0": {
120
+ "anyOf": [
121
+ {
122
+ "$ref": "#/definitions/ExtensionPrimitiveValue"
123
+ },
124
+ {
125
+ "type": "object",
126
+ "maxProperties": 10,
127
+ "propertyNames": {
128
+ "$ref": "#/definitions/ExtensionIdentifier"
129
+ },
130
+ "additionalProperties": {
131
+ "$ref": "#/definitions/ExtensionValueInner1"
132
+ }
133
+ }
134
+ ]
135
+ },
136
+ "ExtensionValueInner1": {
137
+ "anyOf": [
138
+ {
139
+ "$ref": "#/definitions/ExtensionPrimitiveValue"
140
+ }
141
+ ]
142
+ },
143
+ "TagDefinition": {
144
+ "type": "object",
145
+ "description": "Definition of a tag that can be associated with a token via its identifier",
146
+ "additionalProperties": false,
147
+ "properties": {
148
+ "name": {
149
+ "type": "string",
150
+ "description": "The name of the tag",
151
+ "pattern": "^[ \\w]+$",
152
+ "minLength": 1,
153
+ "maxLength": 20
154
+ },
155
+ "description": {
156
+ "type": "string",
157
+ "description": "A user-friendly description of the tag",
158
+ "pattern": "^[ \\w\\.,:]+$",
159
+ "minLength": 1,
160
+ "maxLength": 200
161
+ }
162
+ },
163
+ "required": ["name", "description"],
164
+ "examples": [
165
+ {
166
+ "name": "Stablecoin",
167
+ "description": "A token with value pegged to another asset"
168
+ }
169
+ ]
170
+ },
171
+ "TokenInfo": {
172
+ "type": "object",
173
+ "description": "Metadata for a single token in a token list",
174
+ "additionalProperties": false,
175
+ "properties": {
176
+ "chainId": {
177
+ "type": "integer",
178
+ "description": "The chain ID of the Ethereum network where this token is deployed",
179
+ "minimum": 1,
180
+ "examples": [1, 42]
181
+ },
182
+ "address": {
183
+ "type": "string",
184
+ "description": "The checksummed address of the token on the specified chain ID",
185
+ "pattern": "^0x[a-fA-F0-9]{40}$",
186
+ "examples": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]
187
+ },
188
+ "decimals": {
189
+ "type": "integer",
190
+ "description": "The number of decimals for the token balance",
191
+ "minimum": 0,
192
+ "maximum": 255,
193
+ "examples": [18]
194
+ },
195
+ "name": {
196
+ "type": "string",
197
+ "description": "The name of the token",
198
+ "minLength": 1,
199
+ "maxLength": 40,
200
+ "pattern": "^[ \\w.'+\\-%/À-ÖØ-öø-ÿ:&\\[\\]\\(\\)]+$",
201
+ "examples": ["USD Coin"]
202
+ },
203
+ "symbol": {
204
+ "type": "string",
205
+ "description": "The symbol for the token; must be alphanumeric",
206
+ "pattern": "^[a-zA-Z0-9+\\-%/$.]+$",
207
+ "minLength": 1,
208
+ "maxLength": 20,
209
+ "examples": ["USDC"]
210
+ },
211
+ "logoURI": {
212
+ "type": "string",
213
+ "description": "A URI to the token logo asset; if not set, interface will attempt to find a logo based on the token address; suggest SVG or PNG of size 64x64",
214
+ "format": "uri",
215
+ "examples": ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
216
+ },
217
+ "tags": {
218
+ "type": "array",
219
+ "description": "An array of tag identifiers associated with the token; tags are defined at the list level",
220
+ "items": {
221
+ "$ref": "#/definitions/TagIdentifier"
222
+ },
223
+ "maxItems": 10,
224
+ "examples": ["stablecoin", "compound"]
225
+ },
226
+ "extensions": {
227
+ "$ref": "#/definitions/ExtensionMap"
228
+ }
229
+ },
230
+ "required": ["chainId", "address", "decimals", "name", "symbol"]
231
+ },
232
+ "AptosTokenInfo": {
233
+ "type": "object",
234
+ "description": "Metadata for a single token in a token list",
235
+ "additionalProperties": false,
236
+ "properties": {
237
+ "chainId": {
238
+ "type": "integer",
239
+ "description": "The chain ID of the Aptos network where this token is deployed, 0 is devent",
240
+ "minimum": 0,
241
+ "examples": [1, 42]
242
+ },
243
+ "address": {
244
+ "type": "string",
245
+ "description": "The address of the coin on the specified chain ID",
246
+ "examples": ["0x1::aptos_coin::AptosCoin"]
247
+ },
248
+ "decimals": {
249
+ "type": "integer",
250
+ "description": "The number of decimals for the token balance",
251
+ "minimum": 0,
252
+ "maximum": 255,
253
+ "examples": [18]
254
+ },
255
+ "name": {
256
+ "type": "string",
257
+ "description": "The name of the token",
258
+ "minLength": 1,
259
+ "maxLength": 40,
260
+ "pattern": "^[ \\w.'+\\-%/À-ÖØ-öø-ÿ:&\\[\\]\\(\\)]+$",
261
+ "examples": ["USD Coin"]
262
+ },
263
+ "symbol": {
264
+ "type": "string",
265
+ "description": "The symbol for the token; must be alphanumeric",
266
+ "pattern": "^[a-zA-Z0-9+\\-%/$.]+$",
267
+ "minLength": 1,
268
+ "maxLength": 20,
269
+ "examples": ["USDC"]
270
+ },
271
+ "logoURI": {
272
+ "type": "string",
273
+ "description": "A URI to the token logo asset; if not set, interface will attempt to find a logo based on the token address; suggest SVG or PNG of size 64x64",
274
+ "format": "uri",
275
+ "examples": ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
276
+ },
277
+ "tags": {
278
+ "type": "array",
279
+ "description": "An array of tag identifiers associated with the token; tags are defined at the list level",
280
+ "items": {
281
+ "$ref": "#/definitions/TagIdentifier"
282
+ },
283
+ "maxItems": 10,
284
+ "examples": ["stablecoin", "compound"]
285
+ },
286
+ "extensions": {
287
+ "$ref": "#/definitions/ExtensionMap"
288
+ }
289
+ },
290
+ "required": ["chainId", "address", "decimals", "name", "symbol"]
291
+ }
292
+ },
293
+ "type": "object",
294
+ "additionalProperties": false,
295
+ "properties": {
296
+ "name": {
297
+ "type": "string",
298
+ "description": "The name of the token list",
299
+ "minLength": 1,
300
+ "maxLength": 30,
301
+ "pattern": "^[\\w ]+$",
302
+ "examples": ["My Token List"]
303
+ },
304
+ "timestamp": {
305
+ "type": "string",
306
+ "format": "date-time",
307
+ "description": "The timestamp of this list version; i.e. when this immutable version of the list was created"
308
+ },
309
+ "schema": {
310
+ "type": "string"
311
+ },
312
+ "version": {
313
+ "$ref": "#/definitions/Version"
314
+ },
315
+ "tokens": {
316
+ "type": "array",
317
+ "description": "The list of tokens included in the list",
318
+ "minItems": 1,
319
+ "maxItems": 10000
320
+ },
321
+ "keywords": {
322
+ "type": "array",
323
+ "description": "Keywords associated with the contents of the list; may be used in list discoverability",
324
+ "items": {
325
+ "type": "string",
326
+ "description": "A keyword to describe the contents of the list",
327
+ "minLength": 1,
328
+ "maxLength": 20,
329
+ "pattern": "^[\\w ]+$",
330
+ "examples": ["compound", "lending", "personal tokens"]
331
+ },
332
+ "maxItems": 20,
333
+ "uniqueItems": true
334
+ },
335
+ "tags": {
336
+ "type": "object",
337
+ "description": "A mapping of tag identifiers to their name and description",
338
+ "propertyNames": {
339
+ "$ref": "#/definitions/TagIdentifier"
340
+ },
341
+ "additionalProperties": {
342
+ "$ref": "#/definitions/TagDefinition"
343
+ },
344
+ "maxProperties": 20,
345
+ "examples": [
346
+ {
347
+ "stablecoin": {
348
+ "name": "Stablecoin",
349
+ "description": "A token with value pegged to another asset"
350
+ }
351
+ }
352
+ ]
353
+ },
354
+ "logoURI": {
355
+ "type": "string",
356
+ "description": "A URI for the logo of the token list; prefer SVG or PNG of size 256x256",
357
+ "format": "uri",
358
+ "examples": ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
359
+ }
360
+ },
361
+ "if": {
362
+ "properties": { "schema": { "const": "aptos" } },
363
+ "required": ["name", "timestamp", "version", "tokens", "schema"]
364
+ },
365
+ "then": {
366
+ "properties": {
367
+ "tokens": {
368
+ "items": {
369
+ "$ref": "#/definitions/AptosTokenInfo"
370
+ },
371
+ "type": "array",
372
+ "description": "The list of tokens included in the list",
373
+ "minItems": 1,
374
+ "maxItems": 10000
375
+ }
376
+ }
377
+ },
378
+ "else": {
379
+ "properties": {
380
+ "tokens": {
381
+ "items": {
382
+ "$ref": "#/definitions/TokenInfo"
383
+ },
384
+ "type": "array",
385
+ "description": "The list of tokens included in the list",
386
+ "minItems": 1,
387
+ "maxItems": 10000
388
+ }
389
+ }
390
+ },
391
+ "required": ["name", "timestamp", "version", "tokens"]
392
+ }