@pancakeswap/token-lists 0.0.17 → 0.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.
package/react/lists.ts CHANGED
@@ -3,10 +3,15 @@ import { atom, useAtom, useAtomValue } from 'jotai'
3
3
  import { atomFamily, atomWithStorage, loadable } from 'jotai/utils'
4
4
  import { type AsyncStorage } from 'jotai/vanilla/utils/atomWithStorage'
5
5
  import localForage from 'localforage'
6
- import { fetchTokenList } from './actions'
6
+ import debounce from 'lodash/debounce'
7
+ import {
8
+ fetchTokenList,
9
+ batchFetchTokenListPending,
10
+ batchFetchTokenListFulfilled,
11
+ batchFetchTokenListRejected,
12
+ } from './actions'
7
13
  import { getTokenList } from './getTokenList'
8
14
  import { ListsState } from './reducer'
9
-
10
15
  // eslint-disable-next-line @typescript-eslint/no-empty-function
11
16
  function noop() {}
12
17
 
@@ -41,7 +46,7 @@ export function findTokenBySymbol(state: ListsState, chainId: number, symbol: st
41
46
 
42
47
  export const createListsAtom = (storeName: string, reducer: any, initialState: any) => {
43
48
  /**
44
- * Persist you redux state using IndexedDB
49
+ * Persist only token lists using IndexedDB - optimized storage format
45
50
  * @param {string} dbName - IndexedDB database name
46
51
  */
47
52
  function IndexedDBStorage<Value>(dbName: string): AsyncStorage<Value> {
@@ -50,17 +55,27 @@ export const createListsAtom = (storeName: string, reducer: any, initialState: a
50
55
  name: dbName,
51
56
  storeName,
52
57
  })
58
+ const mem = new Map<string, any>()
59
+
60
+ const debouncedSetItem = debounce(async (k: string, v: any) => {
61
+ db.setItem(k, v)
62
+ }, 300)
63
+
53
64
  return {
54
65
  getItem: async (key: string) => {
66
+ if (mem.has(key)) {
67
+ return mem.get(key)
68
+ }
55
69
  const value = await db.getItem(key)
56
70
  if (value) {
57
71
  return value
58
72
  }
59
- return initialState
73
+ return undefined as any
60
74
  },
61
75
  setItem: async (k: string, v: any) => {
62
76
  if (v === EMPTY) return
63
- await db.setItem(k, v)
77
+ mem.set(k, v)
78
+ debouncedSetItem(k, v)
64
79
  },
65
80
  removeItem: db.removeItem,
66
81
  }
@@ -68,63 +83,166 @@ export const createListsAtom = (storeName: string, reducer: any, initialState: a
68
83
  return noopStorage
69
84
  }
70
85
 
71
- const listsStorageAtom = atomWithStorage<ListsState | typeof EMPTY>('lists', EMPTY, IndexedDBStorage('lists'))
86
+ // Storage for token lists only - optimized format: { [url]: TokenList }
87
+ const tokenListsStorageAtom = atomWithStorage<Record<string, any> | typeof EMPTY>(
88
+ 'tokenLists',
89
+ EMPTY,
90
+ IndexedDBStorage('tokenLists'),
91
+ )
72
92
 
73
- const defaultStateAtom = atom<ListsState, any, void>(
74
- (get) => {
75
- const value = get(loadable(listsStorageAtom))
76
- if (value.state === 'hasData' && value.data !== EMPTY) {
77
- return value.data
93
+ // Memory state atom that holds the full ListsState
94
+ const memoryStateAtom = atom<ListsState>(initialState as ListsState)
95
+
96
+ const listStateAtom = atom<ListsState>((get) => {
97
+ // Separate this(mem/storage) is a prepare for refactor of the list part
98
+ const memoryState = get(memoryStateAtom)
99
+ const value = get(loadable(tokenListsStorageAtom))
100
+
101
+ if (value.state === 'hasData' && value.data && value.data !== EMPTY) {
102
+ const storedTokenLists = value.data as Record<string, any>
103
+ const reconstructedState = { ...memoryState }
104
+
105
+ const updatedByUrl = { ...reconstructedState.byUrl }
106
+ Object.keys(storedTokenLists).forEach((url) => {
107
+ if (storedTokenLists[url]) {
108
+ updatedByUrl[url] = {
109
+ ...updatedByUrl[url],
110
+ current: storedTokenLists[url],
111
+ // Keep existing memory state for loading, error, pendingUpdate
112
+ }
113
+ }
114
+ })
115
+ reconstructedState.byUrl = updatedByUrl
116
+
117
+ return reconstructedState
118
+ }
119
+
120
+ return memoryState
121
+ })
122
+
123
+ const updateListStateAtom = atom<null, any, void>(null, async (get, set, action) => {
124
+ const currentMemoryState = get(memoryStateAtom)
125
+ const newState = reducer(currentMemoryState, action)
126
+
127
+ // Update memory state
128
+ set(memoryStateAtom, { ...newState })
129
+
130
+ // Extract only current token lists for storage
131
+ const tokenListsToStore: Record<string, any> = {}
132
+ Object.keys(newState.byUrl).forEach((url) => {
133
+ if (newState.byUrl[url]?.current) {
134
+ tokenListsToStore[url] = newState.byUrl[url].current
78
135
  }
79
- return initialState
80
- },
81
- async (get, set, action) => {
82
- set(listsStorageAtom, reducer(await get(defaultStateAtom), action))
83
- },
84
- )
136
+ })
137
+
138
+ // Store only the token lists, not the full state
139
+ set(tokenListsStorageAtom, tokenListsToStore)
140
+ })
85
141
 
86
- const isReadyAtom = loadable(listsStorageAtom)
142
+ const isReadyAtom = loadable(tokenListsStorageAtom)
87
143
 
88
144
  const tokenAtom = atomFamily((key: { chainId: number; address: string }) =>
89
- atom((get) => findTokenByAddress(get(defaultStateAtom), key.chainId, key.address)),
145
+ atom((get) => findTokenByAddress(get(listStateAtom), key.chainId, key.address)),
90
146
  )
91
147
 
92
148
  const tokenBySymbolAtom = atomFamily((key: { chainId: number; symbol: string }) =>
93
- atom((get) => findTokenBySymbol(get(defaultStateAtom), key.chainId, key.symbol)),
149
+ atom((get) => findTokenBySymbol(get(listStateAtom), key.chainId, key.symbol)),
94
150
  )
95
151
 
96
152
  const fetchListAtom = atom<null, [string], Promise<void>>(null, async (get, set, url) => {
97
- const state = get(defaultStateAtom)
153
+ const state = get(listStateAtom)
98
154
  const listState = state.byUrl[url]
99
155
  if (listState?.current || listState?.loadingRequestId) {
100
156
  return
101
157
  }
102
158
 
103
159
  const requestId = nanoid()
104
- set(defaultStateAtom, fetchTokenList.pending({ url, requestId }))
160
+ set(updateListStateAtom, fetchTokenList.pending({ url, requestId }))
105
161
 
106
162
  try {
107
163
  const tokenList = await getTokenList(url)
108
- set(defaultStateAtom, fetchTokenList.fulfilled({ url, tokenList: tokenList!, requestId }))
164
+ set(updateListStateAtom, fetchTokenList.fulfilled({ url, tokenList: tokenList!, requestId }))
165
+ } catch (error: any) {
166
+ set(updateListStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }))
167
+ }
168
+ })
169
+
170
+ const fetchListBatchAtom = atom<null, [string[]], Promise<void>>(null, async (get, set, urls) => {
171
+ const state = get(listStateAtom)
172
+
173
+ // Filter out URLs that are already loaded or loading
174
+ const urlsToFetch = urls.filter((url) => {
175
+ const listState = state.byUrl[url]
176
+ return !listState?.current && !listState?.loadingRequestId
177
+ })
178
+
179
+ if (urlsToFetch.length === 0) {
180
+ return
181
+ }
182
+
183
+ const requestId = nanoid()
184
+ set(updateListStateAtom, batchFetchTokenListPending({ urls: urlsToFetch, requestId }))
185
+
186
+ try {
187
+ // Fetch all token lists in parallel
188
+ const results = await Promise.allSettled(
189
+ urlsToFetch.map(async (url) => {
190
+ const tokenList = await getTokenList(url)
191
+ return { url, tokenList: tokenList!, requestId }
192
+ }),
193
+ )
194
+
195
+ // Separate successful and failed results
196
+ const fulfilled: Array<{ url: string; tokenList: any; requestId: string }> = []
197
+ const rejected: Array<{ url: string; errorMessage: string; requestId: string }> = []
198
+
199
+ results.forEach((result, index) => {
200
+ const url = urlsToFetch[index]
201
+ if (result.status === 'fulfilled') {
202
+ fulfilled.push(result.value)
203
+ } else {
204
+ rejected.push({
205
+ url,
206
+ errorMessage: result.reason?.message || 'Unknown error',
207
+ requestId,
208
+ })
209
+ }
210
+ })
211
+
212
+ // Dispatch batch actions
213
+ if (fulfilled.length > 0) {
214
+ set(updateListStateAtom, batchFetchTokenListFulfilled({ results: fulfilled }))
215
+ }
216
+ if (rejected.length > 0) {
217
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors: rejected }))
218
+ }
109
219
  } catch (error: any) {
110
- set(defaultStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }))
220
+ // Fallback to individual rejections if batch processing fails
221
+ const errors = urlsToFetch.map((url) => ({
222
+ url,
223
+ errorMessage: error.message,
224
+ requestId,
225
+ }))
226
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors }))
111
227
  }
112
228
  })
113
229
 
114
230
  function useListState() {
115
- return useAtom(defaultStateAtom)
231
+ return useAtom(listStateAtom)
116
232
  }
117
233
 
118
234
  function useListStateReady() {
119
235
  const value = useAtomValue(isReadyAtom)
120
- return value.state === 'hasData' && value.data !== EMPTY
236
+ return value.state === 'hasData'
121
237
  }
122
238
 
123
239
  return {
124
- listsAtom: defaultStateAtom,
240
+ listsAtom: listStateAtom,
241
+ updateListStateAtom,
125
242
  tokenAtom,
126
243
  tokenBySymbolAtom,
127
244
  fetchListAtom,
245
+ fetchListBatchAtom,
128
246
  useListStateReady,
129
247
  useListState,
130
248
  }
package/react/reducer.ts CHANGED
@@ -8,8 +8,12 @@ import {
8
8
  enableList,
9
9
  disableList,
10
10
  updateListVersion,
11
+ batchFetchTokenListPending,
12
+ batchFetchTokenListFulfilled,
13
+ batchFetchTokenListRejected,
11
14
  } from './actions'
12
- import { getVersionUpgrade, VersionUpgrade, TokenList } from '../src'
15
+ import { TokenList } from '../src/types'
16
+ import { setPendingTokenList, setFulfilledTokenList, setRejectedTokenList, batchActivateUrls } from './reducerHelpers'
13
17
 
14
18
  export interface ListsState {
15
19
  readonly byUrl: {
@@ -44,62 +48,13 @@ export const createTokenListReducer = (
44
48
  createReducer(initialState, (builder) =>
45
49
  builder
46
50
  .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
- }
51
+ setPendingTokenList(state, url, requestId)
56
52
  })
57
53
  .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) && state.activeListUrls && !state.activeListUrls.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
- }
54
+ setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS)
89
55
  })
90
56
  .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
- }
57
+ setRejectedTokenList(state, url, requestId, errorMessage)
103
58
  })
104
59
  .addCase(addList, (state, { payload: url }) => {
105
60
  if (!state.byUrl[url]) {
@@ -182,5 +137,31 @@ export const createTokenListReducer = (
182
137
  return true
183
138
  })
184
139
  }
140
+ })
141
+ .addCase(batchFetchTokenListPending, (state, { payload: { urls, requestId } }) => {
142
+ // Batch update multiple lists to pending state in a single update cycle
143
+ urls.forEach((url) => {
144
+ setPendingTokenList(state, url, requestId)
145
+ })
146
+ })
147
+ .addCase(batchFetchTokenListFulfilled, (state, { payload: { results } }) => {
148
+ // Batch update multiple lists to fulfilled state in a single update cycle
149
+ const urlsToActivate: string[] = []
150
+
151
+ results.forEach(({ url, tokenList, requestId }) => {
152
+ const isNewList = setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS)
153
+ if (isNewList && DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
154
+ urlsToActivate.push(url)
155
+ }
156
+ })
157
+
158
+ // Batch activate URLs if needed
159
+ batchActivateUrls(state, urlsToActivate)
160
+ })
161
+ .addCase(batchFetchTokenListRejected, (state, { payload: { errors } }) => {
162
+ // Batch update multiple lists to rejected state in a single update cycle
163
+ errors.forEach(({ url, requestId, errorMessage }) => {
164
+ setRejectedTokenList(state, url, requestId, errorMessage)
165
+ })
185
166
  }),
186
167
  )
@@ -0,0 +1,111 @@
1
+ import { getVersionUpgrade, VersionUpgrade } from '../src/getVersionUpgrade'
2
+ import { TokenList } from '../src/types'
3
+
4
+ // Mutable state type for reducer operations
5
+ type MutableListsState = {
6
+ byUrl: {
7
+ [url: string]: {
8
+ current: TokenList | null
9
+ pendingUpdate: TokenList | null
10
+ loadingRequestId: string | null
11
+ error: string | null
12
+ }
13
+ }
14
+ lastInitializedDefaultListOfLists?: string[]
15
+ activeListUrls: string[] | undefined
16
+ }
17
+
18
+ /**
19
+ * Core function to set a single list to pending state
20
+ */
21
+ export const setPendingTokenList = (state: MutableListsState, url: string, requestId: string): void => {
22
+ const current = state.byUrl[url]?.current ?? null
23
+ const pendingUpdate = state.byUrl[url]?.pendingUpdate ?? null
24
+
25
+ state.byUrl[url] = {
26
+ current,
27
+ pendingUpdate,
28
+ loadingRequestId: requestId,
29
+ error: null,
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Core function to set a single list to fulfilled state
35
+ */
36
+ export const setFulfilledTokenList = (
37
+ state: MutableListsState,
38
+ url: string,
39
+ tokenList: TokenList,
40
+ requestId: string,
41
+ DEFAULT_ACTIVE_LIST_URLS: string[],
42
+ ): boolean => {
43
+ const current = state.byUrl[url]?.current
44
+ const loadingRequestId = state.byUrl[url]?.loadingRequestId
45
+
46
+ // no-op if update does nothing
47
+ if (current) {
48
+ const upgradeType = getVersionUpgrade(current.version, tokenList.version)
49
+
50
+ if (upgradeType === VersionUpgrade.NONE) return false
51
+ if (loadingRequestId === null || loadingRequestId === requestId) {
52
+ state.byUrl[url] = {
53
+ ...state.byUrl[url],
54
+ loadingRequestId: null,
55
+ error: null,
56
+ current,
57
+ pendingUpdate: tokenList,
58
+ }
59
+ }
60
+ return false // not a new list, so no activation needed
61
+ }
62
+ // activate if on default active
63
+ if (DEFAULT_ACTIVE_LIST_URLS.includes(url) && state.activeListUrls && !state.activeListUrls.includes(url)) {
64
+ state.activeListUrls.push(url)
65
+ }
66
+
67
+ state.byUrl[url] = {
68
+ ...state.byUrl[url],
69
+ loadingRequestId: null,
70
+ error: null,
71
+ current: tokenList,
72
+ pendingUpdate: null,
73
+ }
74
+ return true // new list, might need activation
75
+ }
76
+
77
+ /**
78
+ * Core function to set a single list to rejected state
79
+ */
80
+ export const setRejectedTokenList = (
81
+ state: MutableListsState,
82
+ url: string,
83
+ requestId: string,
84
+ errorMessage: string,
85
+ ): void => {
86
+ if (state.byUrl[url]?.loadingRequestId !== requestId) {
87
+ // no-op since it's not the latest request
88
+ return
89
+ }
90
+
91
+ state.byUrl[url] = {
92
+ ...state.byUrl[url],
93
+ loadingRequestId: null,
94
+ error: errorMessage,
95
+ current: null,
96
+ pendingUpdate: null,
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Batch activate URLs if needed
102
+ */
103
+ export const batchActivateUrls = (state: MutableListsState, urls: string[]): void => {
104
+ if (urls.length > 0 && state.activeListUrls) {
105
+ urls.forEach((url) => {
106
+ if (!state.activeListUrls!.includes(url)) {
107
+ state.activeListUrls!.push(url)
108
+ }
109
+ })
110
+ }
111
+ }
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "major": 1,
13
13
  "minor": 0,
14
- "patch": 0
14
+ "patch": 1
15
15
  }
16
16
  ],
17
17
  "additionalProperties": false,
@@ -196,17 +196,17 @@
196
196
  "type": "string",
197
197
  "description": "The name of the token",
198
198
  "minLength": 1,
199
- "maxLength": 40,
200
- "pattern": "^[ \\w.'+\\-%/À-ÖØ-öø-ÿ:&\\[\\]\\(\\)]+$",
201
- "examples": ["USD Coin"]
199
+ "maxLength": 60,
200
+ "pattern": "^[ \\w.'+\\-%/,\\$À-ÖØ-öø-ÿ:&\\[\\]\\(\\)\\u4e00-\\u9fa5]+$",
201
+ "examples": ["USD Coin", "币安币"]
202
202
  },
203
203
  "symbol": {
204
204
  "type": "string",
205
205
  "description": "The symbol for the token; must be alphanumeric",
206
- "pattern": "^[a-zA-Z0-9+\\-%/$.\\s]+$",
206
+ "pattern": "^[a-zA-Z0-9+\\-%/$.\\s_\\u4e00-\\u9fa5]+$",
207
207
  "minLength": 1,
208
208
  "maxLength": 20,
209
- "examples": ["USDC"]
209
+ "examples": ["USDC", "币安"]
210
210
  },
211
211
  "logoURI": {
212
212
  "type": "string",
@@ -256,14 +256,14 @@
256
256
  "type": "string",
257
257
  "description": "The name of the token",
258
258
  "minLength": 1,
259
- "maxLength": 40,
260
- "pattern": "^[ \\w.'+\\-%/À-ÖØ-öø-ÿ:&\\[\\]\\(\\)]+$",
259
+ "maxLength": 60,
260
+ "pattern": "^[ \\w.'+\\-%/,\\$À-ÖØ-öø-ÿ:&\\[\\]\\(\\)]+$",
261
261
  "examples": ["USD Coin"]
262
262
  },
263
263
  "symbol": {
264
264
  "type": "string",
265
265
  "description": "The symbol for the token; must be alphanumeric",
266
- "pattern": "^[a-zA-Z0-9+\\-%/$.]+$",
266
+ "pattern": "^[a-zA-Z0-9+\\-%/$._]+$",
267
267
  "minLength": 1,
268
268
  "maxLength": 20,
269
269
  "examples": ["USDC"]