@pancakeswap/token-lists 0.0.17 → 0.1.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/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"]
@@ -397,6 +403,15 @@ var pancakeswap_default = {
397
403
  description: "A URI for the logo of the token list; prefer SVG or PNG of size 256x256",
398
404
  format: "uri",
399
405
  examples: ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
406
+ },
407
+ extensions: {
408
+ type: "object",
409
+ description: "An object containing arbitrary or vendor-specific token list metadata",
410
+ propertyNames: {
411
+ $ref: "#/definitions/ExtensionIdentifier"
412
+ },
413
+ additionalProperties: true,
414
+ maxProperties: 10
400
415
  }
401
416
  },
402
417
  if: {
@@ -442,16 +457,15 @@ function getTokenList(listUrl) {
442
457
  try {
443
458
  const json = yield fetchJson(url);
444
459
  if (!validator(json)) {
445
- const preFilterErrors = validator.errors;
446
460
  json.tokens = json.tokens.filter((token) => validator(__spreadProps(__spreadValues({}, json), { tokens: [token] })));
447
461
  if (!validator(json)) {
448
- const errors = validator.errors;
462
+ const { errors } = validator;
449
463
  throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`);
450
464
  }
451
- console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`);
452
465
  }
453
466
  return json;
454
467
  } catch (error) {
468
+ console.warn(`Failed to download or validate list from ${url}:`, error);
455
469
  return void 0;
456
470
  }
457
471
  }
@@ -466,6 +480,47 @@ function fetchJson(url) {
466
480
  return res.json();
467
481
  });
468
482
  }
483
+ function normalizeTokenAddress(address) {
484
+ return address.startsWith("0x") ? address.toLowerCase() : address;
485
+ }
486
+ function getGeoBlockTokenKey(token) {
487
+ return `${token.chainId}:${normalizeTokenAddress(token.address)}`;
488
+ }
489
+ function normalizeTokenKey(key) {
490
+ const separatorIndex = key.indexOf(":");
491
+ if (separatorIndex < 0)
492
+ return key;
493
+ const chainId = key.slice(0, separatorIndex);
494
+ const address = key.slice(separatorIndex + 1);
495
+ return `${chainId}:${normalizeTokenAddress(address)}`;
496
+ }
497
+ function getGeoBlockedTokenKeys(geoBlockList, countryCode) {
498
+ const blockedTokenKeys = /* @__PURE__ */ new Set();
499
+ if (!countryCode || !(geoBlockList == null ? void 0 : geoBlockList.rules) || !geoBlockList.tokens) {
500
+ return blockedTokenKeys;
501
+ }
502
+ const country = countryCode.toUpperCase();
503
+ const blockedRuleIds = new Set(
504
+ Object.entries(geoBlockList.rules).filter(([, countries]) => countries.includes(country)).map(([ruleId]) => ruleId)
505
+ );
506
+ Object.entries(geoBlockList.tokens).forEach(([tokenKey, ruleIds]) => {
507
+ if (ruleIds.some((ruleId) => blockedRuleIds.has(ruleId))) {
508
+ blockedTokenKeys.add(normalizeTokenKey(tokenKey));
509
+ }
510
+ });
511
+ return blockedTokenKeys;
512
+ }
513
+ function isGeoBlockedToken(token, geoBlockList, countryCode) {
514
+ return getGeoBlockedTokenKeys(geoBlockList, countryCode).has(getGeoBlockTokenKey(token));
515
+ }
516
+ function fetchJson2(url) {
517
+ return __async(this, null, function* () {
518
+ const res = yield fetch(url);
519
+ if (!res.ok)
520
+ throw new Error(`Failed to fetch: ${url}`);
521
+ return res.json();
522
+ });
523
+ }
469
524
  function noop() {
470
525
  }
471
526
  var noopStorage = {
@@ -496,82 +551,278 @@ function findTokenBySymbol(state, chainId, symbol) {
496
551
  }
497
552
  return void 0;
498
553
  }
499
- var createListsAtom = (storeName, reducer, initialState) => {
554
+ var createListsAtom = (storeName, reducer, initialState, options = {}) => {
500
555
  function IndexedDBStorage(dbName) {
501
556
  if (typeof window !== "undefined") {
502
557
  const db = localForage.createInstance({
503
558
  name: dbName,
504
559
  storeName
505
560
  });
561
+ const mem = /* @__PURE__ */ new Map();
562
+ const debouncedSetItem = debounce((k, v) => __async(this, null, function* () {
563
+ db.setItem(k, v);
564
+ }), 300);
506
565
  return {
507
566
  getItem: (key) => __async(this, null, function* () {
567
+ if (mem.has(key)) {
568
+ return mem.get(key);
569
+ }
508
570
  const value = yield db.getItem(key);
509
571
  if (value) {
510
572
  return value;
511
573
  }
512
- return initialState;
574
+ return void 0;
513
575
  }),
514
576
  setItem: (k, v) => __async(this, null, function* () {
515
577
  if (v === EMPTY)
516
578
  return;
517
- yield db.setItem(k, v);
579
+ mem.set(k, v);
580
+ debouncedSetItem(k, v);
518
581
  }),
519
582
  removeItem: db.removeItem
520
583
  };
521
584
  }
522
585
  return noopStorage;
523
586
  }
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
- })
587
+ const tokenListsStorageAtom = atomWithStorage(
588
+ "tokenLists",
589
+ EMPTY,
590
+ IndexedDBStorage("tokenLists"),
591
+ { getOnInit: true }
536
592
  );
537
- const isReadyAtom = loadable(listsStorageAtom);
593
+ const geoBlockListStorageAtom = atomWithStorage(
594
+ "geoBlockList",
595
+ EMPTY,
596
+ IndexedDBStorage("geoBlockList")
597
+ );
598
+ const geoBlockListAtom = atom((get) => {
599
+ const value = get(loadable(geoBlockListStorageAtom));
600
+ if (value.state === "hasData" && value.data && value.data !== EMPTY) {
601
+ return value.data;
602
+ }
603
+ return void 0;
604
+ });
605
+ const fetchGeoBlockListAtom = atom(null, (_get, set) => __async(void 0, null, function* () {
606
+ if (!options.geoBlockListUrl) {
607
+ return;
608
+ }
609
+ try {
610
+ set(geoBlockListStorageAtom, yield fetchJson2(options.geoBlockListUrl));
611
+ } catch (error) {
612
+ console.warn(`Failed to download geo block list from ${options.geoBlockListUrl}:`, error);
613
+ }
614
+ }));
615
+ const memoryStateAtom = atom(initialState);
616
+ const mergeStoredData = (baseState, storedData) => {
617
+ var _a;
618
+ const storedTokenLists = (_a = storedData.byUrl) != null ? _a : storedData;
619
+ const storedActiveListUrls = Array.isArray(storedData.activeListUrls) ? storedData.activeListUrls : void 0;
620
+ const mergedState = __spreadValues({}, baseState);
621
+ const updatedByUrl = __spreadValues({}, mergedState.byUrl);
622
+ Object.keys(storedTokenLists).forEach((url) => {
623
+ if (storedTokenLists[url]) {
624
+ updatedByUrl[url] = __spreadProps(__spreadValues({}, updatedByUrl[url]), {
625
+ current: storedTokenLists[url]
626
+ // Keep existing memory state for loading, error, pendingUpdate
627
+ });
628
+ }
629
+ });
630
+ mergedState.byUrl = updatedByUrl;
631
+ if (storedActiveListUrls) {
632
+ mergedState.activeListUrls = storedActiveListUrls;
633
+ }
634
+ return mergedState;
635
+ };
636
+ const listStateAtom = atom((get) => {
637
+ const memoryState = get(memoryStateAtom);
638
+ const value = get(loadable(tokenListsStorageAtom));
639
+ if (value.state === "hasData" && value.data && value.data !== EMPTY) {
640
+ return mergeStoredData(memoryState, value.data);
641
+ }
642
+ return memoryState;
643
+ });
644
+ let hasHydratedActiveListUrls = false;
645
+ const updateListStateAtom = atom(null, (get, set, action) => __async(void 0, null, function* () {
646
+ const baseState = get(memoryStateAtom);
647
+ if (!hasHydratedActiveListUrls) {
648
+ const storageValue = get(loadable(tokenListsStorageAtom));
649
+ if (storageValue.state !== "hasData") {
650
+ const optimisticState = reducer(baseState, action);
651
+ set(memoryStateAtom, __spreadValues({}, optimisticState));
652
+ return;
653
+ }
654
+ if (storageValue.data && storageValue.data !== EMPTY) {
655
+ set(memoryStateAtom, mergeStoredData(baseState, storageValue.data));
656
+ }
657
+ hasHydratedActiveListUrls = true;
658
+ }
659
+ const currentState = get(memoryStateAtom);
660
+ const newState = reducer(currentState, action);
661
+ set(memoryStateAtom, __spreadValues({}, newState));
662
+ const tokenListsToStore = {};
663
+ Object.keys(newState.byUrl).forEach((url) => {
664
+ var _a;
665
+ if ((_a = newState.byUrl[url]) == null ? void 0 : _a.current) {
666
+ tokenListsToStore[url] = newState.byUrl[url].current;
667
+ }
668
+ });
669
+ set(tokenListsStorageAtom, { byUrl: tokenListsToStore, activeListUrls: newState.activeListUrls });
670
+ }));
671
+ const isReadyAtom = loadable(tokenListsStorageAtom);
538
672
  const tokenAtom = atomFamily(
539
- (key) => atom((get) => findTokenByAddress(get(defaultStateAtom), key.chainId, key.address))
673
+ (key) => atom((get) => findTokenByAddress(get(listStateAtom), key.chainId, key.address))
540
674
  );
541
675
  const tokenBySymbolAtom = atomFamily(
542
- (key) => atom((get) => findTokenBySymbol(get(defaultStateAtom), key.chainId, key.symbol))
676
+ (key) => atom((get) => findTokenBySymbol(get(listStateAtom), key.chainId, key.symbol))
543
677
  );
544
678
  const fetchListAtom = atom(null, (get, set, url) => __async(void 0, null, function* () {
545
- const state = get(defaultStateAtom);
679
+ const state = get(listStateAtom);
546
680
  const listState = state.byUrl[url];
547
681
  if ((listState == null ? void 0 : listState.current) || (listState == null ? void 0 : listState.loadingRequestId)) {
548
682
  return;
549
683
  }
550
684
  const requestId = nanoid();
551
- set(defaultStateAtom, fetchTokenList.pending({ url, requestId }));
685
+ set(updateListStateAtom, fetchTokenList.pending({ url, requestId }));
552
686
  try {
553
687
  const tokenList = yield getTokenList(url);
554
- set(defaultStateAtom, fetchTokenList.fulfilled({ url, tokenList, requestId }));
688
+ set(updateListStateAtom, fetchTokenList.fulfilled({ url, tokenList, requestId }));
555
689
  } catch (error) {
556
- set(defaultStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }));
690
+ set(updateListStateAtom, fetchTokenList.rejected({ url, requestId, errorMessage: error.message }));
691
+ }
692
+ }));
693
+ const fetchListBatchAtom = atom(null, (get, set, urls) => __async(void 0, null, function* () {
694
+ const state = get(listStateAtom);
695
+ const urlsToFetch = urls.filter((url) => {
696
+ const listState = state.byUrl[url];
697
+ return !(listState == null ? void 0 : listState.current) && !(listState == null ? void 0 : listState.loadingRequestId);
698
+ });
699
+ if (urlsToFetch.length === 0) {
700
+ return;
701
+ }
702
+ const requestId = nanoid();
703
+ set(updateListStateAtom, batchFetchTokenListPending({ urls: urlsToFetch, requestId }));
704
+ try {
705
+ const results = yield Promise.allSettled(
706
+ urlsToFetch.map((url) => __async(void 0, null, function* () {
707
+ const tokenList = yield getTokenList(url);
708
+ return { url, tokenList, requestId };
709
+ }))
710
+ );
711
+ const fulfilled = [];
712
+ const rejected = [];
713
+ results.forEach((result, index) => {
714
+ var _a;
715
+ const url = urlsToFetch[index];
716
+ if (result.status === "fulfilled") {
717
+ fulfilled.push(result.value);
718
+ } else {
719
+ rejected.push({
720
+ url,
721
+ errorMessage: ((_a = result.reason) == null ? void 0 : _a.message) || "Unknown error",
722
+ requestId
723
+ });
724
+ }
725
+ });
726
+ if (fulfilled.length > 0) {
727
+ set(updateListStateAtom, batchFetchTokenListFulfilled({ results: fulfilled }));
728
+ }
729
+ if (rejected.length > 0) {
730
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors: rejected }));
731
+ }
732
+ } catch (error) {
733
+ const errors = urlsToFetch.map((url) => ({
734
+ url,
735
+ errorMessage: error.message,
736
+ requestId
737
+ }));
738
+ set(updateListStateAtom, batchFetchTokenListRejected({ errors }));
557
739
  }
558
740
  }));
559
741
  function useListState() {
560
- return useAtom(defaultStateAtom);
742
+ return useAtom(listStateAtom);
561
743
  }
562
744
  function useListStateReady() {
563
745
  const value = useAtomValue(isReadyAtom);
564
- return value.state === "hasData" && value.data !== EMPTY;
746
+ return value.state === "hasData";
565
747
  }
566
748
  return {
567
- listsAtom: defaultStateAtom,
749
+ listsAtom: listStateAtom,
750
+ updateListStateAtom,
568
751
  tokenAtom,
569
752
  tokenBySymbolAtom,
753
+ geoBlockListAtom,
754
+ fetchGeoBlockListAtom,
570
755
  fetchListAtom,
756
+ fetchListBatchAtom,
571
757
  useListStateReady,
572
758
  useListState
573
759
  };
574
760
  };
761
+
762
+ // react/reducerHelpers.ts
763
+ var setPendingTokenList = (state, url, requestId) => {
764
+ var _a, _b, _c, _d;
765
+ const current = (_b = (_a = state.byUrl[url]) == null ? void 0 : _a.current) != null ? _b : null;
766
+ const pendingUpdate = (_d = (_c = state.byUrl[url]) == null ? void 0 : _c.pendingUpdate) != null ? _d : null;
767
+ state.byUrl[url] = {
768
+ current,
769
+ pendingUpdate,
770
+ loadingRequestId: requestId,
771
+ error: null
772
+ };
773
+ };
774
+ var setFulfilledTokenList = (state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS) => {
775
+ var _a, _b;
776
+ const current = (_a = state.byUrl[url]) == null ? void 0 : _a.current;
777
+ const loadingRequestId = (_b = state.byUrl[url]) == null ? void 0 : _b.loadingRequestId;
778
+ if (current) {
779
+ const upgradeType = getVersionUpgrade(current.version, tokenList.version);
780
+ if (upgradeType === 0 /* NONE */)
781
+ return false;
782
+ if (loadingRequestId === null || loadingRequestId === requestId) {
783
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
784
+ loadingRequestId: null,
785
+ error: null,
786
+ current,
787
+ pendingUpdate: tokenList
788
+ });
789
+ }
790
+ return false;
791
+ }
792
+ if (DEFAULT_ACTIVE_LIST_URLS.includes(url) && state.activeListUrls && !state.activeListUrls.includes(url)) {
793
+ state.activeListUrls.push(url);
794
+ }
795
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
796
+ loadingRequestId: null,
797
+ error: null,
798
+ current: tokenList,
799
+ pendingUpdate: null
800
+ });
801
+ return true;
802
+ };
803
+ var setRejectedTokenList = (state, url, requestId, errorMessage) => {
804
+ var _a;
805
+ if (((_a = state.byUrl[url]) == null ? void 0 : _a.loadingRequestId) !== requestId) {
806
+ return;
807
+ }
808
+ state.byUrl[url] = __spreadProps(__spreadValues({}, state.byUrl[url]), {
809
+ loadingRequestId: null,
810
+ error: errorMessage,
811
+ current: null,
812
+ pendingUpdate: null
813
+ });
814
+ };
815
+ var batchActivateUrls = (state, urls) => {
816
+ if (urls.length > 0 && state.activeListUrls) {
817
+ urls.forEach((url) => {
818
+ if (!state.activeListUrls.includes(url)) {
819
+ state.activeListUrls.push(url);
820
+ }
821
+ });
822
+ }
823
+ };
824
+
825
+ // react/reducer.ts
575
826
  var NEW_LIST_STATE = {
576
827
  error: null,
577
828
  current: null,
@@ -581,53 +832,11 @@ var NEW_LIST_STATE = {
581
832
  var createTokenListReducer = (initialState, DEFAULT_LIST_OF_LISTS, DEFAULT_ACTIVE_LIST_URLS) => createReducer(
582
833
  initialState,
583
834
  (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
- };
835
+ setPendingTokenList(state, url, requestId);
593
836
  }).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
- }
837
+ setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS);
620
838
  }).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
- });
839
+ setRejectedTokenList(state, url, requestId, errorMessage);
631
840
  }).addCase(addList, (state, { payload: url }) => {
632
841
  if (!state.byUrl[url]) {
633
842
  state.byUrl[url] = NEW_LIST_STATE;
@@ -693,6 +902,23 @@ var createTokenListReducer = (initialState, DEFAULT_LIST_OF_LISTS, DEFAULT_ACTIV
693
902
  return true;
694
903
  });
695
904
  }
905
+ }).addCase(batchFetchTokenListPending, (state, { payload: { urls, requestId } }) => {
906
+ urls.forEach((url) => {
907
+ setPendingTokenList(state, url, requestId);
908
+ });
909
+ }).addCase(batchFetchTokenListFulfilled, (state, { payload: { results } }) => {
910
+ const urlsToActivate = [];
911
+ results.forEach(({ url, tokenList, requestId }) => {
912
+ const isNewList = setFulfilledTokenList(state, url, tokenList, requestId, DEFAULT_ACTIVE_LIST_URLS);
913
+ if (isNewList && DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
914
+ urlsToActivate.push(url);
915
+ }
916
+ });
917
+ batchActivateUrls(state, urlsToActivate);
918
+ }).addCase(batchFetchTokenListRejected, (state, { payload: { errors } }) => {
919
+ errors.forEach(({ url, requestId, errorMessage }) => {
920
+ setRejectedTokenList(state, url, requestId, errorMessage);
921
+ });
696
922
  })
697
923
  );
698
924
  function useFetchListCallback(dispatch) {
@@ -711,7 +937,7 @@ function useFetchListCallback(dispatch) {
711
937
  }
712
938
  return tokenList;
713
939
  }).catch((error) => {
714
- console.error(`Failed to get list at url ${listUrl}`, error);
940
+ console.warn(`Failed to get list at url ${listUrl}`, error);
715
941
  if (sendDispatch) {
716
942
  dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }));
717
943
  }
@@ -723,4 +949,4 @@ function useFetchListCallback(dispatch) {
723
949
  }
724
950
  var useFetchListCallback_default = useFetchListCallback;
725
951
 
726
- export { NEW_LIST_STATE, acceptListUpdate, addList, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getTokenList, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback_default as useFetchListCallback };
952
+ export { NEW_LIST_STATE, acceptListUpdate, addList, batchFetchTokenListFulfilled, batchFetchTokenListPending, batchFetchTokenListRejected, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getGeoBlockTokenKey, getGeoBlockedTokenKeys, getTokenList, isGeoBlockedToken, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback_default as useFetchListCallback };
@@ -35,6 +35,7 @@ interface TokenList {
35
35
  readonly keywords?: string[];
36
36
  readonly tags?: Tags;
37
37
  readonly logoURI?: string;
38
+ readonly extensions?: Record<string, unknown>;
38
39
  }
39
40
 
40
41
  export { TokenInfo as T, Version as V, TokenList as a, Tags as b };
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.1",
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.1"
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.3"
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')
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getGeoBlockedTokenKeys, getGeoBlockTokenKey, isGeoBlockedToken } from './lists'
3
+
4
+ const GEO_BLOCK_LIST = {
5
+ rules: {
6
+ A: ['US'],
7
+ B: ['SG'],
8
+ },
9
+ tokens: {
10
+ '56:0xa0Fe4e0aEca5479705ce996615B2EACB6b6a10Fb': ['A'],
11
+ '8000001001:Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh': ['A'],
12
+ '56:0x0000000000000000000000000000000000000001': ['B'],
13
+ },
14
+ }
15
+
16
+ describe('geo block helpers', () => {
17
+ it('normalizes EVM addresses and preserves non-EVM addresses', () => {
18
+ expect(
19
+ getGeoBlockTokenKey({
20
+ chainId: 56,
21
+ address: '0xa0Fe4e0aEca5479705ce996615B2EACB6b6a10Fb',
22
+ }),
23
+ ).toBe('56:0xa0fe4e0aeca5479705ce996615b2eacb6b6a10fb')
24
+
25
+ expect(
26
+ getGeoBlockTokenKey({
27
+ chainId: 8000001001,
28
+ address: 'Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh',
29
+ }),
30
+ ).toBe('8000001001:Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh')
31
+ })
32
+
33
+ it('returns blocked token keys for the active country', () => {
34
+ expect([...getGeoBlockedTokenKeys(GEO_BLOCK_LIST, 'US')].sort()).toEqual([
35
+ '56:0xa0fe4e0aeca5479705ce996615b2eacb6b6a10fb',
36
+ '8000001001:Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh',
37
+ ])
38
+ })
39
+
40
+ it('fails open without country or geo block list', () => {
41
+ expect(getGeoBlockedTokenKeys(GEO_BLOCK_LIST).size).toBe(0)
42
+ expect(getGeoBlockedTokenKeys(undefined, 'US').size).toBe(0)
43
+ })
44
+
45
+ it('checks whether a token is geo blocked', () => {
46
+ expect(
47
+ isGeoBlockedToken(
48
+ {
49
+ chainId: 56,
50
+ address: '0xa0Fe4e0aEca5479705ce996615B2EACB6b6a10Fb',
51
+ },
52
+ GEO_BLOCK_LIST,
53
+ 'US',
54
+ ),
55
+ ).toBe(true)
56
+
57
+ expect(
58
+ isGeoBlockedToken(
59
+ {
60
+ chainId: 56,
61
+ address: '0xa0Fe4e0aEca5479705ce996615B2EACB6b6a10Fb',
62
+ },
63
+ GEO_BLOCK_LIST,
64
+ 'SG',
65
+ ),
66
+ ).toBe(false)
67
+ })
68
+ })