@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/dist/react.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { __async, __spreadProps, __spreadValues, getVersionUpgrade } from './chunk-DC45HQOL.mjs';
1
+ import { __async, __spreadProps, __spreadValues, getVersionUpgrade } from './chunk-UUVLERWJ.mjs';
2
2
  import { createAction, nanoid, createReducer } from '@reduxjs/toolkit';
3
3
  import { atom, useAtom, useAtomValue } from 'jotai';
4
4
  import { atomWithStorage, loadable, atomFamily } from 'jotai/utils';
5
5
  import localForage from 'localforage';
6
+ import debounce from 'lodash/debounce';
6
7
  import { useCallback } from 'react';
7
8
 
8
9
  var fetchTokenList = {
@@ -17,6 +18,11 @@ var disableList = createAction("lists/disableList");
17
18
  var acceptListUpdate = createAction("lists/acceptListUpdate");
18
19
  var rejectVersionUpdate = createAction("lists/rejectVersionUpdate");
19
20
  var updateListVersion = createAction("lists/updateListVersion");
21
+ var batchFetchTokenListPending = createAction(
22
+ "lists/batchFetchTokenList/pending"
23
+ );
24
+ var batchFetchTokenListFulfilled = createAction("lists/batchFetchTokenList/fulfilled");
25
+ var batchFetchTokenListRejected = createAction("lists/batchFetchTokenList/rejected");
20
26
 
21
27
  // ../utils/uriToHttp.ts
22
28
  function uriToHttp(uri) {
@@ -52,7 +58,7 @@ var pancakeswap_default = {
52
58
  {
53
59
  major: 1,
54
60
  minor: 0,
55
- patch: 0
61
+ patch: 1
56
62
  }
57
63
  ],
58
64
  additionalProperties: false,
@@ -237,17 +243,17 @@ var pancakeswap_default = {
237
243
  type: "string",
238
244
  description: "The name of the token",
239
245
  minLength: 1,
240
- maxLength: 40,
241
- pattern: "^[ \\w.'+\\-%/\xC0-\xD6\xD8-\xF6\xF8-\xFF:&\\[\\]\\(\\)]+$",
242
- examples: ["USD Coin"]
246
+ maxLength: 60,
247
+ pattern: "^[ \\w.'+\\-%/,\\$\xC0-\xD6\xD8-\xF6\xF8-\xFF:&\\[\\]\\(\\)\\u4e00-\\u9fa5]+$",
248
+ examples: ["USD Coin", "\u5E01\u5B89\u5E01"]
243
249
  },
244
250
  symbol: {
245
251
  type: "string",
246
252
  description: "The symbol for the token; must be alphanumeric",
247
- pattern: "^[a-zA-Z0-9+\\-%/$.\\s]+$",
253
+ pattern: "^[a-zA-Z0-9+\\-%/$.\\s_\\u4e00-\\u9fa5]+$",
248
254
  minLength: 1,
249
255
  maxLength: 20,
250
- examples: ["USDC"]
256
+ examples: ["USDC", "\u5E01\u5B89"]
251
257
  },
252
258
  logoURI: {
253
259
  type: "string",
@@ -297,14 +303,14 @@ var pancakeswap_default = {
297
303
  type: "string",
298
304
  description: "The name of the token",
299
305
  minLength: 1,
300
- maxLength: 40,
301
- pattern: "^[ \\w.'+\\-%/\xC0-\xD6\xD8-\xF6\xF8-\xFF:&\\[\\]\\(\\)]+$",
306
+ maxLength: 60,
307
+ pattern: "^[ \\w.'+\\-%/,\\$\xC0-\xD6\xD8-\xF6\xF8-\xFF:&\\[\\]\\(\\)]+$",
302
308
  examples: ["USD Coin"]
303
309
  },
304
310
  symbol: {
305
311
  type: "string",
306
312
  description: "The symbol for the token; must be alphanumeric",
307
- pattern: "^[a-zA-Z0-9+\\-%/$.]+$",
313
+ pattern: "^[a-zA-Z0-9+\\-%/$._]+$",
308
314
  minLength: 1,
309
315
  maxLength: 20,
310
316
  examples: ["USDC"]
@@ -445,13 +451,14 @@ function getTokenList(listUrl) {
445
451
  const preFilterErrors = validator.errors;
446
452
  json.tokens = json.tokens.filter((token) => validator(__spreadProps(__spreadValues({}, json), { tokens: [token] })));
447
453
  if (!validator(json)) {
448
- const errors = validator.errors;
454
+ const { errors } = validator;
449
455
  throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`);
450
456
  }
451
457
  console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`);
452
458
  }
453
459
  return json;
454
460
  } catch (error) {
461
+ console.error(`Failed to download or validate list from ${url}:`, error);
455
462
  return void 0;
456
463
  }
457
464
  }
@@ -503,75 +510,224 @@ var createListsAtom = (storeName, reducer, initialState) => {
503
510
  name: dbName,
504
511
  storeName
505
512
  });
513
+ const mem = /* @__PURE__ */ new Map();
514
+ const debouncedSetItem = debounce((k, v) => __async(this, null, function* () {
515
+ db.setItem(k, v);
516
+ }), 300);
506
517
  return {
507
518
  getItem: (key) => __async(this, null, function* () {
519
+ if (mem.has(key)) {
520
+ return mem.get(key);
521
+ }
508
522
  const value = yield db.getItem(key);
509
523
  if (value) {
510
524
  return value;
511
525
  }
512
- return initialState;
526
+ return void 0;
513
527
  }),
514
528
  setItem: (k, v) => __async(this, null, function* () {
515
529
  if (v === EMPTY)
516
530
  return;
517
- yield db.setItem(k, v);
531
+ mem.set(k, v);
532
+ debouncedSetItem(k, v);
518
533
  }),
519
534
  removeItem: db.removeItem
520
535
  };
521
536
  }
522
537
  return noopStorage;
523
538
  }
524
- const listsStorageAtom = atomWithStorage("lists", EMPTY, IndexedDBStorage("lists"));
525
- const defaultStateAtom = atom(
526
- (get) => {
527
- const value = get(loadable(listsStorageAtom));
528
- if (value.state === "hasData" && value.data !== EMPTY) {
529
- return value.data;
530
- }
531
- return initialState;
532
- },
533
- (get, set, action) => __async(void 0, null, function* () {
534
- set(listsStorageAtom, reducer(yield get(defaultStateAtom), action));
535
- })
539
+ const tokenListsStorageAtom = atomWithStorage(
540
+ "tokenLists",
541
+ EMPTY,
542
+ IndexedDBStorage("tokenLists")
536
543
  );
537
- const isReadyAtom = loadable(listsStorageAtom);
544
+ const memoryStateAtom = atom(initialState);
545
+ const listStateAtom = atom((get) => {
546
+ const memoryState = get(memoryStateAtom);
547
+ const value = get(loadable(tokenListsStorageAtom));
548
+ if (value.state === "hasData" && value.data && value.data !== EMPTY) {
549
+ const storedTokenLists = value.data;
550
+ const reconstructedState = __spreadValues({}, memoryState);
551
+ const updatedByUrl = __spreadValues({}, reconstructedState.byUrl);
552
+ Object.keys(storedTokenLists).forEach((url) => {
553
+ if (storedTokenLists[url]) {
554
+ updatedByUrl[url] = __spreadProps(__spreadValues({}, updatedByUrl[url]), {
555
+ current: storedTokenLists[url]
556
+ // Keep existing memory state for loading, error, pendingUpdate
557
+ });
558
+ }
559
+ });
560
+ reconstructedState.byUrl = updatedByUrl;
561
+ return reconstructedState;
562
+ }
563
+ return memoryState;
564
+ });
565
+ const updateListStateAtom = atom(null, (get, set, action) => __async(void 0, null, function* () {
566
+ const currentMemoryState = get(memoryStateAtom);
567
+ const newState = reducer(currentMemoryState, action);
568
+ set(memoryStateAtom, __spreadValues({}, newState));
569
+ const tokenListsToStore = {};
570
+ Object.keys(newState.byUrl).forEach((url) => {
571
+ var _a;
572
+ if ((_a = newState.byUrl[url]) == null ? void 0 : _a.current) {
573
+ tokenListsToStore[url] = newState.byUrl[url].current;
574
+ }
575
+ });
576
+ set(tokenListsStorageAtom, tokenListsToStore);
577
+ }));
578
+ const isReadyAtom = loadable(tokenListsStorageAtom);
538
579
  const tokenAtom = atomFamily(
539
- (key) => atom((get) => findTokenByAddress(get(defaultStateAtom), key.chainId, key.address))
580
+ (key) => atom((get) => findTokenByAddress(get(listStateAtom), key.chainId, key.address))
540
581
  );
541
582
  const tokenBySymbolAtom = atomFamily(
542
- (key) => atom((get) => findTokenBySymbol(get(defaultStateAtom), key.chainId, key.symbol))
583
+ (key) => atom((get) => findTokenBySymbol(get(listStateAtom), key.chainId, key.symbol))
543
584
  );
544
585
  const fetchListAtom = atom(null, (get, set, url) => __async(void 0, null, function* () {
545
- const state = get(defaultStateAtom);
586
+ const state = get(listStateAtom);
546
587
  const listState = state.byUrl[url];
547
588
  if ((listState == null ? void 0 : listState.current) || (listState == null ? void 0 : listState.loadingRequestId)) {
548
589
  return;
549
590
  }
550
591
  const requestId = nanoid();
551
- set(defaultStateAtom, fetchTokenList.pending({ url, requestId }));
592
+ set(updateListStateAtom, fetchTokenList.pending({ url, requestId }));
552
593
  try {
553
594
  const tokenList = yield getTokenList(url);
554
- set(defaultStateAtom, fetchTokenList.fulfilled({ url, tokenList, requestId }));
595
+ set(updateListStateAtom, fetchTokenList.fulfilled({ url, tokenList, requestId }));
555
596
  } catch (error) {
556
- set(defaultStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }));
597
+ set(updateListStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }));
598
+ }
599
+ }));
600
+ const fetchListBatchAtom = atom(null, (get, set, urls) => __async(void 0, null, function* () {
601
+ const state = get(listStateAtom);
602
+ const urlsToFetch = urls.filter((url) => {
603
+ const listState = state.byUrl[url];
604
+ return !(listState == null ? void 0 : listState.current) && !(listState == null ? void 0 : listState.loadingRequestId);
605
+ });
606
+ if (urlsToFetch.length === 0) {
607
+ return;
608
+ }
609
+ const requestId = nanoid();
610
+ set(updateListStateAtom, batchFetchTokenListPending({ urls: urlsToFetch, requestId }));
611
+ try {
612
+ const results = yield Promise.allSettled(
613
+ urlsToFetch.map((url) => __async(void 0, null, function* () {
614
+ const tokenList = yield getTokenList(url);
615
+ return { url, tokenList, requestId };
616
+ }))
617
+ );
618
+ const fulfilled = [];
619
+ const rejected = [];
620
+ results.forEach((result, index) => {
621
+ var _a;
622
+ const url = urlsToFetch[index];
623
+ if (result.status === "fulfilled") {
624
+ fulfilled.push(result.value);
625
+ } else {
626
+ rejected.push({
627
+ url,
628
+ errorMessage: ((_a = result.reason) == null ? void 0 : _a.message) || "Unknown error",
629
+ requestId
630
+ });
631
+ }
632
+ });
633
+ if (fulfilled.length > 0) {
634
+ set(updateListStateAtom, batchFetchTokenListFulfilled({ results: fulfilled }));
635
+ }
636
+ if (rejected.length > 0) {
637
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors: rejected }));
638
+ }
639
+ } catch (error) {
640
+ const errors = urlsToFetch.map((url) => ({
641
+ url,
642
+ errorMessage: error.message,
643
+ requestId
644
+ }));
645
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors }));
557
646
  }
558
647
  }));
559
648
  function useListState() {
560
- return useAtom(defaultStateAtom);
649
+ return useAtom(listStateAtom);
561
650
  }
562
651
  function useListStateReady() {
563
652
  const value = useAtomValue(isReadyAtom);
564
- return value.state === "hasData" && value.data !== EMPTY;
653
+ return value.state === "hasData";
565
654
  }
566
655
  return {
567
- listsAtom: defaultStateAtom,
656
+ listsAtom: listStateAtom,
657
+ updateListStateAtom,
568
658
  tokenAtom,
569
659
  tokenBySymbolAtom,
570
660
  fetchListAtom,
661
+ fetchListBatchAtom,
571
662
  useListStateReady,
572
663
  useListState
573
664
  };
574
665
  };
666
+
667
+ // react/reducerHelpers.ts
668
+ var setPendingTokenList = (state, url, requestId) => {
669
+ var _a, _b, _c, _d;
670
+ const current = (_b = (_a = state.byUrl[url]) == null ? void 0 : _a.current) != null ? _b : null;
671
+ const pendingUpdate = (_d = (_c = state.byUrl[url]) == null ? void 0 : _c.pendingUpdate) != null ? _d : null;
672
+ state.byUrl[url] = {
673
+ current,
674
+ pendingUpdate,
675
+ loadingRequestId: requestId,
676
+ error: null
677
+ };
678
+ };
679
+ var setFulfilledTokenList = (state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS) => {
680
+ var _a, _b;
681
+ const current = (_a = state.byUrl[url]) == null ? void 0 : _a.current;
682
+ const loadingRequestId = (_b = state.byUrl[url]) == null ? void 0 : _b.loadingRequestId;
683
+ if (current) {
684
+ const upgradeType = getVersionUpgrade(current.version, tokenList.version);
685
+ if (upgradeType === 0 /* NONE */)
686
+ return false;
687
+ if (loadingRequestId === null || loadingRequestId === requestId) {
688
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
689
+ loadingRequestId: null,
690
+ error: null,
691
+ current,
692
+ pendingUpdate: tokenList
693
+ });
694
+ }
695
+ return false;
696
+ }
697
+ if (DEFAULT_ACTIVE_LIST_URLS.includes(url) && state.activeListUrls && !state.activeListUrls.includes(url)) {
698
+ state.activeListUrls.push(url);
699
+ }
700
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
701
+ loadingRequestId: null,
702
+ error: null,
703
+ current: tokenList,
704
+ pendingUpdate: null
705
+ });
706
+ return true;
707
+ };
708
+ var setRejectedTokenList = (state, url, requestId, errorMessage) => {
709
+ var _a;
710
+ if (((_a = state.byUrl[url]) == null ? void 0 : _a.loadingRequestId) !== requestId) {
711
+ return;
712
+ }
713
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
714
+ loadingRequestId: null,
715
+ error: errorMessage,
716
+ current: null,
717
+ pendingUpdate: null
718
+ });
719
+ };
720
+ var batchActivateUrls = (state, urls) => {
721
+ if (urls.length > 0 && state.activeListUrls) {
722
+ urls.forEach((url) => {
723
+ if (!state.activeListUrls.includes(url)) {
724
+ state.activeListUrls.push(url);
725
+ }
726
+ });
727
+ }
728
+ };
729
+
730
+ // react/reducer.ts
575
731
  var NEW_LIST_STATE = {
576
732
  error: null,
577
733
  current: null,
@@ -581,53 +737,11 @@ var NEW_LIST_STATE = {
581
737
  var createTokenListReducer = (initialState, DEFAULT_LIST_OF_LISTS, DEFAULT_ACTIVE_LIST_URLS) => createReducer(
582
738
  initialState,
583
739
  (builder) => builder.addCase(fetchTokenList.pending, (state, { payload: { requestId, url } }) => {
584
- var _a, _b, _c, _d;
585
- const current = (_b = (_a = state.byUrl[url]) == null ? void 0 : _a.current) != null ? _b : null;
586
- const pendingUpdate = (_d = (_c = state.byUrl[url]) == null ? void 0 : _c.pendingUpdate) != null ? _d : null;
587
- state.byUrl[url] = {
588
- current,
589
- pendingUpdate,
590
- loadingRequestId: requestId,
591
- error: null
592
- };
740
+ setPendingTokenList(state, url, requestId);
593
741
  }).addCase(fetchTokenList.fulfilled, (state, { payload: { requestId, tokenList, url } }) => {
594
- var _a, _b;
595
- const current = (_a = state.byUrl[url]) == null ? void 0 : _a.current;
596
- const loadingRequestId = (_b = state.byUrl[url]) == null ? void 0 : _b.loadingRequestId;
597
- if (current) {
598
- const upgradeType = getVersionUpgrade(current.version, tokenList.version);
599
- if (upgradeType === 0 /* NONE */)
600
- return;
601
- if (loadingRequestId === null || loadingRequestId === requestId) {
602
- state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
603
- loadingRequestId: null,
604
- error: null,
605
- current,
606
- pendingUpdate: tokenList
607
- });
608
- }
609
- } else {
610
- if (DEFAULT_ACTIVE_LIST_URLS.includes(url) && state.activeListUrls && !state.activeListUrls.includes(url)) {
611
- state.activeListUrls.push(url);
612
- }
613
- state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
614
- loadingRequestId: null,
615
- error: null,
616
- current: tokenList,
617
- pendingUpdate: null
618
- });
619
- }
742
+ setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS);
620
743
  }).addCase(fetchTokenList.rejected, (state, { payload: { url, requestId, errorMessage } }) => {
621
- var _a;
622
- if (((_a = state.byUrl[url]) == null ? void 0 : _a.loadingRequestId) !== requestId) {
623
- return;
624
- }
625
- state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
626
- loadingRequestId: null,
627
- error: errorMessage,
628
- current: null,
629
- pendingUpdate: null
630
- });
744
+ setRejectedTokenList(state, url, requestId, errorMessage);
631
745
  }).addCase(addList, (state, { payload: url }) => {
632
746
  if (!state.byUrl[url]) {
633
747
  state.byUrl[url] = NEW_LIST_STATE;
@@ -693,6 +807,23 @@ var createTokenListReducer = (initialState, DEFAULT_LIST_OF_LISTS, DEFAULT_ACTIV
693
807
  return true;
694
808
  });
695
809
  }
810
+ }).addCase(batchFetchTokenListPending, (state, { payload: { urls, requestId } }) => {
811
+ urls.forEach((url) => {
812
+ setPendingTokenList(state, url, requestId);
813
+ });
814
+ }).addCase(batchFetchTokenListFulfilled, (state, { payload: { results } }) => {
815
+ const urlsToActivate = [];
816
+ results.forEach(({ url, tokenList, requestId }) => {
817
+ const isNewList = setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS);
818
+ if (isNewList && DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
819
+ urlsToActivate.push(url);
820
+ }
821
+ });
822
+ batchActivateUrls(state, urlsToActivate);
823
+ }).addCase(batchFetchTokenListRejected, (state, { payload: { errors } }) => {
824
+ errors.forEach(({ url, requestId, errorMessage }) => {
825
+ setRejectedTokenList(state, url, requestId, errorMessage);
826
+ });
696
827
  })
697
828
  );
698
829
  function useFetchListCallback(dispatch) {
@@ -723,4 +854,4 @@ function useFetchListCallback(dispatch) {
723
854
  }
724
855
  var useFetchListCallback_default = useFetchListCallback;
725
856
 
726
- export { NEW_LIST_STATE, acceptListUpdate, addList, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getTokenList, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback_default as useFetchListCallback };
857
+ export { NEW_LIST_STATE, acceptListUpdate, addList, batchFetchTokenListFulfilled, batchFetchTokenListPending, batchFetchTokenListRejected, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getTokenList, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback_default as useFetchListCallback };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@pancakeswap/token-lists",
3
- "version": "0.0.17",
3
+ "version": "0.1.0",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
7
7
  "dependencies": {
8
8
  "ajv": "^6.12.3",
9
9
  "lodash": "^4.17.21",
10
- "@pancakeswap/swap-sdk-core": "1.5.1"
10
+ "@pancakeswap/swap-sdk-core": "1.6.0"
11
11
  },
12
12
  "peerDependencies": {
13
13
  "@reduxjs/toolkit": "^1.9.1",
@@ -22,7 +22,8 @@
22
22
  "localforage": "^1.10.0",
23
23
  "react": "^18.2.0",
24
24
  "tsup": "^6.7.0",
25
- "@pancakeswap/utils": "8.0.1"
25
+ "@types/lodash": "^4.14.182",
26
+ "@pancakeswap/utils": "8.0.2"
26
27
  },
27
28
  "exports": {
28
29
  ".": {
package/react/actions.ts CHANGED
@@ -23,3 +23,14 @@ export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
23
23
  export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
24
24
 
25
25
  export const updateListVersion = createAction('lists/updateListVersion')
26
+
27
+ // Batch actions for reducing update cycles
28
+ export const batchFetchTokenListPending = createAction<{ urls: string[]; requestId: string }>(
29
+ 'lists/batchFetchTokenList/pending',
30
+ )
31
+ export const batchFetchTokenListFulfilled = createAction<{
32
+ results: Array<{ url: string; tokenList: TokenList; requestId: string }>
33
+ }>('lists/batchFetchTokenList/fulfilled')
34
+ export const batchFetchTokenListRejected = createAction<{
35
+ errors: Array<{ url: string; errorMessage: string; requestId: string }>
36
+ }>('lists/batchFetchTokenList/rejected')
@@ -20,7 +20,7 @@ export async function getTokenList(listUrl: string): Promise<TokenList | undefin
20
20
  const preFilterErrors = validator.errors
21
21
  json.tokens = json.tokens.filter((token: any) => validator({ ...json, tokens: [token] }))
22
22
  if (!validator(json)) {
23
- const errors = validator.errors
23
+ const { errors } = validator
24
24
  throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`)
25
25
  }
26
26
  console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`)
@@ -30,6 +30,7 @@ export async function getTokenList(listUrl: string): Promise<TokenList | undefin
30
30
  // if (i === urls.length - 1) {
31
31
  // throw new Error(`Failed to download list ${listUrl}`)
32
32
  // }
33
+ console.error(`Failed to download or validate list from ${url}:`, error)
33
34
 
34
35
  return undefined
35
36
  }
@@ -12,6 +12,9 @@ test('exports', () => {
12
12
  "acceptListUpdate",
13
13
  "rejectVersionUpdate",
14
14
  "updateListVersion",
15
+ "batchFetchTokenListPending",
16
+ "batchFetchTokenListFulfilled",
17
+ "batchFetchTokenListRejected",
15
18
  "getTokenList",
16
19
  "findTokenByAddress",
17
20
  "findTokenBySymbol",