@pancakeswap/token-lists 0.1.0 → 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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.js +11 -0
- package/dist/index.mjs +11 -1
- package/dist/react.d.ts +21 -3
- package/dist/react.js +120 -22
- package/dist/react.mjs +118 -23
- package/dist/{types-92a8b029.d.ts → types-dcc7ec4a.d.ts} +1 -0
- package/package.json +3 -3
- package/react/geoBlock.test.ts +68 -0
- package/react/getTokenList.test.ts +54 -0
- package/react/getTokenList.ts +1 -3
- package/react/index.test.ts +3 -0
- package/react/lists.ts +186 -24
- package/react/useFetchListCallback.ts +1 -1
- package/schema/pancakeswap.json +9 -0
- package/src/index.test.ts +1 -0
- package/src/index.ts +1 -0
- package/src/scaledUIAmount.ts +41 -0
- package/src/types.ts +1 -0
- package/src/wrappedTokenInfo.ts +8 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @pancakeswap/token-lists@0.1.
|
|
2
|
+
> @pancakeswap/token-lists@0.1.1 build /home/runner/work/pancake-frontend/pancake-frontend/packages/token-lists
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
CLI Building entry: {"index":"./src/index.ts","react":"./react/index.ts"}
|
|
@@ -10,15 +10,15 @@ CLI Target: es6
|
|
|
10
10
|
ESM Build start
|
|
11
11
|
CJS Build start
|
|
12
12
|
CJS dist/chunk-Y2NTTLTJ.js 2.30 KB
|
|
13
|
-
CJS dist/index.js 2.
|
|
14
|
-
CJS dist/react.js
|
|
15
|
-
CJS ⚡️ Build success in
|
|
13
|
+
CJS dist/index.js 2.68 KB
|
|
14
|
+
CJS dist/react.js 33.13 KB
|
|
15
|
+
CJS ⚡️ Build success in 3052ms
|
|
16
16
|
ESM dist/chunk-UUVLERWJ.mjs 2.18 KB
|
|
17
|
-
ESM dist/
|
|
18
|
-
ESM dist/
|
|
19
|
-
ESM ⚡️ Build success in
|
|
17
|
+
ESM dist/index.mjs 2.28 KB
|
|
18
|
+
ESM dist/react.mjs 31.72 KB
|
|
19
|
+
ESM ⚡️ Build success in 3052ms
|
|
20
20
|
DTS Build start
|
|
21
|
-
DTS ⚡️ Build success in
|
|
22
|
-
DTS dist/index.d.ts
|
|
23
|
-
DTS dist/react.d.ts
|
|
24
|
-
DTS dist/types-
|
|
21
|
+
DTS ⚡️ Build success in 32179ms
|
|
22
|
+
DTS dist/index.d.ts 3.23 KB
|
|
23
|
+
DTS dist/react.d.ts 5.01 KB
|
|
24
|
+
DTS dist/types-dcc7ec4a.d.ts 1.11 KB
|
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SerializedToken, Token } from '@pancakeswap/swap-sdk-core';
|
|
2
|
-
import { T as TokenInfo, a as TokenList, V as Version } from './types-
|
|
3
|
-
export { b as Tags } from './types-
|
|
2
|
+
import { T as TokenInfo, a as TokenList, V as Version } from './types-dcc7ec4a.js';
|
|
3
|
+
export { b as Tags } from './types-dcc7ec4a.js';
|
|
4
4
|
|
|
5
5
|
interface SerializedWrappedToken extends SerializedToken {
|
|
6
6
|
chainId: number;
|
|
@@ -16,6 +16,12 @@ interface SerializedWrappedToken extends SerializedToken {
|
|
|
16
16
|
*/
|
|
17
17
|
declare class WrappedTokenInfo extends Token {
|
|
18
18
|
readonly logoURI: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Preserves the token-list `extensions` bag (curator-set metadata like
|
|
21
|
+
* `bridgeInfo`, `scaledUIAmount.enabled`, etc.). Stored as a readonly reference;
|
|
22
|
+
* not serialised back out of the class (kept in-memory only).
|
|
23
|
+
*/
|
|
24
|
+
readonly extensions: TokenInfo['extensions'];
|
|
19
25
|
constructor(tokenInfo: TokenInfo);
|
|
20
26
|
get serialize(): SerializedWrappedToken;
|
|
21
27
|
}
|
|
@@ -49,4 +55,30 @@ declare function getVersionUpgrade(base: Version, update: Version): VersionUpgra
|
|
|
49
55
|
|
|
50
56
|
declare function createFilterToken<T extends TokenInfo | Token>(search: string, isAddress: (address: string) => boolean): (token: T) => boolean;
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Static metadata identifying an ERC-8056 (Scaled UI Amount) token in a token list.
|
|
60
|
+
* `enabled: true` is the only field FE consumes for routing decisions; everything
|
|
61
|
+
* else is hint/snapshot data refreshed live from chain.
|
|
62
|
+
*/
|
|
63
|
+
interface ScaledUIAmountExtension {
|
|
64
|
+
/** Set to `true` for tokens that implement ERC-8056. The load-bearing FE check. */
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
/** Optional snapshot of the live multiplier at the time of indexing (18-decimal fixed). */
|
|
67
|
+
multiplier?: string;
|
|
68
|
+
/** Optional human-readable multiplier (e.g. `"2.0"`). Display hint only. */
|
|
69
|
+
multiplierFormatted?: string;
|
|
70
|
+
/** ERC-165 interface IDs the contract claims to support. */
|
|
71
|
+
interfaceIds?: string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns true if a token-list entry is flagged as ERC-8056 enabled by the indexer.
|
|
75
|
+
* Tokens lacking the extension or with `enabled: false` are treated as standard ERC-20s
|
|
76
|
+
* (identity multiplier `1.0×`); the FE never falls into ScaledUI math for these.
|
|
77
|
+
*
|
|
78
|
+
* Note: the load-bearing decision lives here, not on the `Token` class. Token
|
|
79
|
+
* instances stay pure identity objects per the architectural decision in the
|
|
80
|
+
* implementation doc.
|
|
81
|
+
*/
|
|
82
|
+
declare function isScaledUIAmountToken(token: Pick<TokenInfo, 'extensions'>): boolean;
|
|
83
|
+
|
|
84
|
+
export { ScaledUIAmountExtension, SerializedWrappedToken, TokenAddressMap, TokenInfo, TokenList, Version, VersionUpgrade, WrappedTokenInfo, createFilterToken, deserializeToken, getVersionUpgrade, isScaledUIAmountToken };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var WrappedTokenInfo = class extends swapSdkCore.Token {
|
|
|
7
7
|
constructor(tokenInfo) {
|
|
8
8
|
super(tokenInfo.chainId, tokenInfo.address, tokenInfo.decimals, tokenInfo.symbol, tokenInfo.name);
|
|
9
9
|
this.logoURI = tokenInfo.logoURI;
|
|
10
|
+
this.extensions = tokenInfo.extensions;
|
|
10
11
|
}
|
|
11
12
|
get serialize() {
|
|
12
13
|
return {
|
|
@@ -61,6 +62,15 @@ function createFilterToken(search, isAddress) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// src/scaledUIAmount.ts
|
|
66
|
+
function isScaledUIAmountToken(token) {
|
|
67
|
+
var _a;
|
|
68
|
+
const ext = (_a = token.extensions) == null ? void 0 : _a.scaledUIAmount;
|
|
69
|
+
if (!ext || typeof ext !== "object" || Array.isArray(ext))
|
|
70
|
+
return false;
|
|
71
|
+
return ext.enabled === true;
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
Object.defineProperty(exports, 'VersionUpgrade', {
|
|
65
75
|
enumerable: true,
|
|
66
76
|
get: function () { return chunkY2NTTLTJ_js.VersionUpgrade; }
|
|
@@ -72,3 +82,4 @@ Object.defineProperty(exports, 'getVersionUpgrade', {
|
|
|
72
82
|
exports.WrappedTokenInfo = WrappedTokenInfo;
|
|
73
83
|
exports.createFilterToken = createFilterToken;
|
|
74
84
|
exports.deserializeToken = deserializeToken;
|
|
85
|
+
exports.isScaledUIAmountToken = isScaledUIAmountToken;
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ var WrappedTokenInfo = class extends Token {
|
|
|
5
5
|
constructor(tokenInfo) {
|
|
6
6
|
super(tokenInfo.chainId, tokenInfo.address, tokenInfo.decimals, tokenInfo.symbol, tokenInfo.name);
|
|
7
7
|
this.logoURI = tokenInfo.logoURI;
|
|
8
|
+
this.extensions = tokenInfo.extensions;
|
|
8
9
|
}
|
|
9
10
|
get serialize() {
|
|
10
11
|
return {
|
|
@@ -59,4 +60,13 @@ function createFilterToken(search, isAddress) {
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
// src/scaledUIAmount.ts
|
|
64
|
+
function isScaledUIAmountToken(token) {
|
|
65
|
+
var _a;
|
|
66
|
+
const ext = (_a = token.extensions) == null ? void 0 : _a.scaledUIAmount;
|
|
67
|
+
if (!ext || typeof ext !== "object" || Array.isArray(ext))
|
|
68
|
+
return false;
|
|
69
|
+
return ext.enabled === true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { WrappedTokenInfo, createFilterToken, deserializeToken, isScaledUIAmountToken };
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _reduxjs_toolkit from '@reduxjs/toolkit';
|
|
2
2
|
import { ActionCreatorWithPayload } from '@reduxjs/toolkit';
|
|
3
|
-
import { a as TokenList, V as Version, T as TokenInfo } from './types-
|
|
3
|
+
import { a as TokenList, V as Version, T as TokenInfo } from './types-dcc7ec4a.js';
|
|
4
4
|
import * as jotai_vanilla_utils_atomFamily from 'jotai/vanilla/utils/atomFamily';
|
|
5
5
|
import * as jotai from 'jotai';
|
|
6
6
|
import * as _reduxjs_toolkit_dist_createReducer from '@reduxjs/toolkit/dist/createReducer';
|
|
@@ -69,9 +69,23 @@ type ListByUrlState = ListsState['byUrl'][string];
|
|
|
69
69
|
declare const NEW_LIST_STATE: ListByUrlState;
|
|
70
70
|
declare const createTokenListReducer: (initialState: ListsState, DEFAULT_LIST_OF_LISTS: string[], DEFAULT_ACTIVE_LIST_URLS: string[]) => _reduxjs_toolkit_dist_createReducer.ReducerWithInitialState<ListsState>;
|
|
71
71
|
|
|
72
|
+
type GeoBlockList = {
|
|
73
|
+
rules?: Record<string, string[]>;
|
|
74
|
+
tokens?: Record<string, string[]>;
|
|
75
|
+
};
|
|
76
|
+
type CreateListsAtomOptions = {
|
|
77
|
+
geoBlockListUrl?: string;
|
|
78
|
+
};
|
|
79
|
+
type TokenKeyLike = {
|
|
80
|
+
chainId: number;
|
|
81
|
+
address: string;
|
|
82
|
+
};
|
|
83
|
+
declare function getGeoBlockTokenKey(token: TokenKeyLike): string;
|
|
84
|
+
declare function getGeoBlockedTokenKeys(geoBlockList?: GeoBlockList, countryCode?: string): Set<string>;
|
|
85
|
+
declare function isGeoBlockedToken(token: TokenKeyLike, geoBlockList?: GeoBlockList, countryCode?: string): boolean;
|
|
72
86
|
declare function findTokenByAddress(state: ListsState, chainId: number, address: string): TokenInfo | undefined;
|
|
73
87
|
declare function findTokenBySymbol(state: ListsState, chainId: number, symbol: string): TokenInfo | undefined;
|
|
74
|
-
declare const createListsAtom: (storeName: string, reducer: any, initialState: any) => {
|
|
88
|
+
declare const createListsAtom: (storeName: string, reducer: any, initialState: any, options?: CreateListsAtomOptions) => {
|
|
75
89
|
listsAtom: jotai.Atom<ListsState>;
|
|
76
90
|
updateListStateAtom: jotai.WritableAtom<null, any, void> & {
|
|
77
91
|
init: null;
|
|
@@ -84,6 +98,10 @@ declare const createListsAtom: (storeName: string, reducer: any, initialState: a
|
|
|
84
98
|
chainId: number;
|
|
85
99
|
symbol: string;
|
|
86
100
|
}, jotai.Atom<TokenInfo | undefined>>;
|
|
101
|
+
geoBlockListAtom: jotai.Atom<GeoBlockList | undefined>;
|
|
102
|
+
fetchGeoBlockListAtom: jotai.WritableAtom<null, [], Promise<void>> & {
|
|
103
|
+
init: null;
|
|
104
|
+
};
|
|
87
105
|
fetchListAtom: jotai.WritableAtom<null, [string], Promise<void>> & {
|
|
88
106
|
init: null;
|
|
89
107
|
};
|
|
@@ -96,4 +114,4 @@ declare const createListsAtom: (storeName: string, reducer: any, initialState: a
|
|
|
96
114
|
|
|
97
115
|
declare function useFetchListCallback(dispatch: (action?: unknown) => void): (listUrl: string, sendDispatch?: boolean) => Promise<TokenList>;
|
|
98
116
|
|
|
99
|
-
export { ListByUrlState, ListsState, NEW_LIST_STATE, acceptListUpdate, addList, batchFetchTokenListFulfilled, batchFetchTokenListPending, batchFetchTokenListRejected, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getTokenList, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback };
|
|
117
|
+
export { CreateListsAtomOptions, GeoBlockList, ListByUrlState, ListsState, NEW_LIST_STATE, acceptListUpdate, addList, batchFetchTokenListFulfilled, batchFetchTokenListPending, batchFetchTokenListRejected, createListsAtom, createTokenListReducer, disableList, enableList, fetchTokenList, findTokenByAddress, findTokenBySymbol, getGeoBlockTokenKey, getGeoBlockedTokenKeys, getTokenList, isGeoBlockedToken, rejectVersionUpdate, removeList, updateListVersion, useFetchListCallback };
|
package/dist/react.js
CHANGED
|
@@ -410,6 +410,15 @@ var pancakeswap_default = {
|
|
|
410
410
|
description: "A URI for the logo of the token list; prefer SVG or PNG of size 256x256",
|
|
411
411
|
format: "uri",
|
|
412
412
|
examples: ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
|
|
413
|
+
},
|
|
414
|
+
extensions: {
|
|
415
|
+
type: "object",
|
|
416
|
+
description: "An object containing arbitrary or vendor-specific token list metadata",
|
|
417
|
+
propertyNames: {
|
|
418
|
+
$ref: "#/definitions/ExtensionIdentifier"
|
|
419
|
+
},
|
|
420
|
+
additionalProperties: true,
|
|
421
|
+
maxProperties: 10
|
|
413
422
|
}
|
|
414
423
|
},
|
|
415
424
|
if: {
|
|
@@ -455,17 +464,15 @@ function getTokenList(listUrl) {
|
|
|
455
464
|
try {
|
|
456
465
|
const json = yield fetchJson(url);
|
|
457
466
|
if (!validator(json)) {
|
|
458
|
-
const preFilterErrors = validator.errors;
|
|
459
467
|
json.tokens = json.tokens.filter((token) => validator(chunkY2NTTLTJ_js.__spreadProps(chunkY2NTTLTJ_js.__spreadValues({}, json), { tokens: [token] })));
|
|
460
468
|
if (!validator(json)) {
|
|
461
469
|
const { errors } = validator;
|
|
462
470
|
throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`);
|
|
463
471
|
}
|
|
464
|
-
console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`);
|
|
465
472
|
}
|
|
466
473
|
return json;
|
|
467
474
|
} catch (error) {
|
|
468
|
-
console.
|
|
475
|
+
console.warn(`Failed to download or validate list from ${url}:`, error);
|
|
469
476
|
return void 0;
|
|
470
477
|
}
|
|
471
478
|
}
|
|
@@ -480,6 +487,47 @@ function fetchJson(url) {
|
|
|
480
487
|
return res.json();
|
|
481
488
|
});
|
|
482
489
|
}
|
|
490
|
+
function normalizeTokenAddress(address) {
|
|
491
|
+
return address.startsWith("0x") ? address.toLowerCase() : address;
|
|
492
|
+
}
|
|
493
|
+
function getGeoBlockTokenKey(token) {
|
|
494
|
+
return `${token.chainId}:${normalizeTokenAddress(token.address)}`;
|
|
495
|
+
}
|
|
496
|
+
function normalizeTokenKey(key) {
|
|
497
|
+
const separatorIndex = key.indexOf(":");
|
|
498
|
+
if (separatorIndex < 0)
|
|
499
|
+
return key;
|
|
500
|
+
const chainId = key.slice(0, separatorIndex);
|
|
501
|
+
const address = key.slice(separatorIndex + 1);
|
|
502
|
+
return `${chainId}:${normalizeTokenAddress(address)}`;
|
|
503
|
+
}
|
|
504
|
+
function getGeoBlockedTokenKeys(geoBlockList, countryCode) {
|
|
505
|
+
const blockedTokenKeys = /* @__PURE__ */ new Set();
|
|
506
|
+
if (!countryCode || !(geoBlockList == null ? void 0 : geoBlockList.rules) || !geoBlockList.tokens) {
|
|
507
|
+
return blockedTokenKeys;
|
|
508
|
+
}
|
|
509
|
+
const country = countryCode.toUpperCase();
|
|
510
|
+
const blockedRuleIds = new Set(
|
|
511
|
+
Object.entries(geoBlockList.rules).filter(([, countries]) => countries.includes(country)).map(([ruleId]) => ruleId)
|
|
512
|
+
);
|
|
513
|
+
Object.entries(geoBlockList.tokens).forEach(([tokenKey, ruleIds]) => {
|
|
514
|
+
if (ruleIds.some((ruleId) => blockedRuleIds.has(ruleId))) {
|
|
515
|
+
blockedTokenKeys.add(normalizeTokenKey(tokenKey));
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return blockedTokenKeys;
|
|
519
|
+
}
|
|
520
|
+
function isGeoBlockedToken(token, geoBlockList, countryCode) {
|
|
521
|
+
return getGeoBlockedTokenKeys(geoBlockList, countryCode).has(getGeoBlockTokenKey(token));
|
|
522
|
+
}
|
|
523
|
+
function fetchJson2(url) {
|
|
524
|
+
return chunkY2NTTLTJ_js.__async(this, null, function* () {
|
|
525
|
+
const res = yield fetch(url);
|
|
526
|
+
if (!res.ok)
|
|
527
|
+
throw new Error(`Failed to fetch: ${url}`);
|
|
528
|
+
return res.json();
|
|
529
|
+
});
|
|
530
|
+
}
|
|
483
531
|
function noop() {
|
|
484
532
|
}
|
|
485
533
|
var noopStorage = {
|
|
@@ -510,7 +558,7 @@ function findTokenBySymbol(state, chainId, symbol) {
|
|
|
510
558
|
}
|
|
511
559
|
return void 0;
|
|
512
560
|
}
|
|
513
|
-
var createListsAtom = (storeName, reducer, initialState) => {
|
|
561
|
+
var createListsAtom = (storeName, reducer, initialState, options = {}) => {
|
|
514
562
|
function IndexedDBStorage(dbName) {
|
|
515
563
|
if (typeof window !== "undefined") {
|
|
516
564
|
const db = localForage__default.default.createInstance({
|
|
@@ -546,32 +594,77 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
546
594
|
const tokenListsStorageAtom = utils.atomWithStorage(
|
|
547
595
|
"tokenLists",
|
|
548
596
|
EMPTY,
|
|
549
|
-
IndexedDBStorage("tokenLists")
|
|
597
|
+
IndexedDBStorage("tokenLists"),
|
|
598
|
+
{ getOnInit: true }
|
|
550
599
|
);
|
|
600
|
+
const geoBlockListStorageAtom = utils.atomWithStorage(
|
|
601
|
+
"geoBlockList",
|
|
602
|
+
EMPTY,
|
|
603
|
+
IndexedDBStorage("geoBlockList")
|
|
604
|
+
);
|
|
605
|
+
const geoBlockListAtom = jotai.atom((get) => {
|
|
606
|
+
const value = get(utils.loadable(geoBlockListStorageAtom));
|
|
607
|
+
if (value.state === "hasData" && value.data && value.data !== EMPTY) {
|
|
608
|
+
return value.data;
|
|
609
|
+
}
|
|
610
|
+
return void 0;
|
|
611
|
+
});
|
|
612
|
+
const fetchGeoBlockListAtom = jotai.atom(null, (_get, set) => chunkY2NTTLTJ_js.__async(void 0, null, function* () {
|
|
613
|
+
if (!options.geoBlockListUrl) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
set(geoBlockListStorageAtom, yield fetchJson2(options.geoBlockListUrl));
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.warn(`Failed to download geo block list from ${options.geoBlockListUrl}:`, error);
|
|
620
|
+
}
|
|
621
|
+
}));
|
|
551
622
|
const memoryStateAtom = jotai.atom(initialState);
|
|
623
|
+
const mergeStoredData = (baseState, storedData) => {
|
|
624
|
+
var _a;
|
|
625
|
+
const storedTokenLists = (_a = storedData.byUrl) != null ? _a : storedData;
|
|
626
|
+
const storedActiveListUrls = Array.isArray(storedData.activeListUrls) ? storedData.activeListUrls : void 0;
|
|
627
|
+
const mergedState = chunkY2NTTLTJ_js.__spreadValues({}, baseState);
|
|
628
|
+
const updatedByUrl = chunkY2NTTLTJ_js.__spreadValues({}, mergedState.byUrl);
|
|
629
|
+
Object.keys(storedTokenLists).forEach((url) => {
|
|
630
|
+
if (storedTokenLists[url]) {
|
|
631
|
+
updatedByUrl[url] = chunkY2NTTLTJ_js.__spreadProps(chunkY2NTTLTJ_js.__spreadValues({}, updatedByUrl[url]), {
|
|
632
|
+
current: storedTokenLists[url]
|
|
633
|
+
// Keep existing memory state for loading, error, pendingUpdate
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
mergedState.byUrl = updatedByUrl;
|
|
638
|
+
if (storedActiveListUrls) {
|
|
639
|
+
mergedState.activeListUrls = storedActiveListUrls;
|
|
640
|
+
}
|
|
641
|
+
return mergedState;
|
|
642
|
+
};
|
|
552
643
|
const listStateAtom = jotai.atom((get) => {
|
|
553
644
|
const memoryState = get(memoryStateAtom);
|
|
554
645
|
const value = get(utils.loadable(tokenListsStorageAtom));
|
|
555
646
|
if (value.state === "hasData" && value.data && value.data !== EMPTY) {
|
|
556
|
-
|
|
557
|
-
const reconstructedState = chunkY2NTTLTJ_js.__spreadValues({}, memoryState);
|
|
558
|
-
const updatedByUrl = chunkY2NTTLTJ_js.__spreadValues({}, reconstructedState.byUrl);
|
|
559
|
-
Object.keys(storedTokenLists).forEach((url) => {
|
|
560
|
-
if (storedTokenLists[url]) {
|
|
561
|
-
updatedByUrl[url] = chunkY2NTTLTJ_js.__spreadProps(chunkY2NTTLTJ_js.__spreadValues({}, updatedByUrl[url]), {
|
|
562
|
-
current: storedTokenLists[url]
|
|
563
|
-
// Keep existing memory state for loading, error, pendingUpdate
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
reconstructedState.byUrl = updatedByUrl;
|
|
568
|
-
return reconstructedState;
|
|
647
|
+
return mergeStoredData(memoryState, value.data);
|
|
569
648
|
}
|
|
570
649
|
return memoryState;
|
|
571
650
|
});
|
|
651
|
+
let hasHydratedActiveListUrls = false;
|
|
572
652
|
const updateListStateAtom = jotai.atom(null, (get, set, action) => chunkY2NTTLTJ_js.__async(void 0, null, function* () {
|
|
573
|
-
const
|
|
574
|
-
|
|
653
|
+
const baseState = get(memoryStateAtom);
|
|
654
|
+
if (!hasHydratedActiveListUrls) {
|
|
655
|
+
const storageValue = get(utils.loadable(tokenListsStorageAtom));
|
|
656
|
+
if (storageValue.state !== "hasData") {
|
|
657
|
+
const optimisticState = reducer(baseState, action);
|
|
658
|
+
set(memoryStateAtom, chunkY2NTTLTJ_js.__spreadValues({}, optimisticState));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (storageValue.data && storageValue.data !== EMPTY) {
|
|
662
|
+
set(memoryStateAtom, mergeStoredData(baseState, storageValue.data));
|
|
663
|
+
}
|
|
664
|
+
hasHydratedActiveListUrls = true;
|
|
665
|
+
}
|
|
666
|
+
const currentState = get(memoryStateAtom);
|
|
667
|
+
const newState = reducer(currentState, action);
|
|
575
668
|
set(memoryStateAtom, chunkY2NTTLTJ_js.__spreadValues({}, newState));
|
|
576
669
|
const tokenListsToStore = {};
|
|
577
670
|
Object.keys(newState.byUrl).forEach((url) => {
|
|
@@ -580,7 +673,7 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
580
673
|
tokenListsToStore[url] = newState.byUrl[url].current;
|
|
581
674
|
}
|
|
582
675
|
});
|
|
583
|
-
set(tokenListsStorageAtom, tokenListsToStore);
|
|
676
|
+
set(tokenListsStorageAtom, { byUrl: tokenListsToStore, activeListUrls: newState.activeListUrls });
|
|
584
677
|
}));
|
|
585
678
|
const isReadyAtom = utils.loadable(tokenListsStorageAtom);
|
|
586
679
|
const tokenAtom = utils.atomFamily(
|
|
@@ -664,6 +757,8 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
664
757
|
updateListStateAtom,
|
|
665
758
|
tokenAtom,
|
|
666
759
|
tokenBySymbolAtom,
|
|
760
|
+
geoBlockListAtom,
|
|
761
|
+
fetchGeoBlockListAtom,
|
|
667
762
|
fetchListAtom,
|
|
668
763
|
fetchListBatchAtom,
|
|
669
764
|
useListStateReady,
|
|
@@ -849,7 +944,7 @@ function useFetchListCallback(dispatch) {
|
|
|
849
944
|
}
|
|
850
945
|
return tokenList;
|
|
851
946
|
}).catch((error) => {
|
|
852
|
-
console.
|
|
947
|
+
console.warn(`Failed to get list at url ${listUrl}`, error);
|
|
853
948
|
if (sendDispatch) {
|
|
854
949
|
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }));
|
|
855
950
|
}
|
|
@@ -874,7 +969,10 @@ exports.enableList = enableList;
|
|
|
874
969
|
exports.fetchTokenList = fetchTokenList;
|
|
875
970
|
exports.findTokenByAddress = findTokenByAddress;
|
|
876
971
|
exports.findTokenBySymbol = findTokenBySymbol;
|
|
972
|
+
exports.getGeoBlockTokenKey = getGeoBlockTokenKey;
|
|
973
|
+
exports.getGeoBlockedTokenKeys = getGeoBlockedTokenKeys;
|
|
877
974
|
exports.getTokenList = getTokenList;
|
|
975
|
+
exports.isGeoBlockedToken = isGeoBlockedToken;
|
|
878
976
|
exports.rejectVersionUpdate = rejectVersionUpdate;
|
|
879
977
|
exports.removeList = removeList;
|
|
880
978
|
exports.updateListVersion = updateListVersion;
|
package/dist/react.mjs
CHANGED
|
@@ -403,6 +403,15 @@ var pancakeswap_default = {
|
|
|
403
403
|
description: "A URI for the logo of the token list; prefer SVG or PNG of size 256x256",
|
|
404
404
|
format: "uri",
|
|
405
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
|
|
406
415
|
}
|
|
407
416
|
},
|
|
408
417
|
if: {
|
|
@@ -448,17 +457,15 @@ function getTokenList(listUrl) {
|
|
|
448
457
|
try {
|
|
449
458
|
const json = yield fetchJson(url);
|
|
450
459
|
if (!validator(json)) {
|
|
451
|
-
const preFilterErrors = validator.errors;
|
|
452
460
|
json.tokens = json.tokens.filter((token) => validator(__spreadProps(__spreadValues({}, json), { tokens: [token] })));
|
|
453
461
|
if (!validator(json)) {
|
|
454
462
|
const { errors } = validator;
|
|
455
463
|
throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`);
|
|
456
464
|
}
|
|
457
|
-
console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`);
|
|
458
465
|
}
|
|
459
466
|
return json;
|
|
460
467
|
} catch (error) {
|
|
461
|
-
console.
|
|
468
|
+
console.warn(`Failed to download or validate list from ${url}:`, error);
|
|
462
469
|
return void 0;
|
|
463
470
|
}
|
|
464
471
|
}
|
|
@@ -473,6 +480,47 @@ function fetchJson(url) {
|
|
|
473
480
|
return res.json();
|
|
474
481
|
});
|
|
475
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
|
+
}
|
|
476
524
|
function noop() {
|
|
477
525
|
}
|
|
478
526
|
var noopStorage = {
|
|
@@ -503,7 +551,7 @@ function findTokenBySymbol(state, chainId, symbol) {
|
|
|
503
551
|
}
|
|
504
552
|
return void 0;
|
|
505
553
|
}
|
|
506
|
-
var createListsAtom = (storeName, reducer, initialState) => {
|
|
554
|
+
var createListsAtom = (storeName, reducer, initialState, options = {}) => {
|
|
507
555
|
function IndexedDBStorage(dbName) {
|
|
508
556
|
if (typeof window !== "undefined") {
|
|
509
557
|
const db = localForage.createInstance({
|
|
@@ -539,32 +587,77 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
539
587
|
const tokenListsStorageAtom = atomWithStorage(
|
|
540
588
|
"tokenLists",
|
|
541
589
|
EMPTY,
|
|
542
|
-
IndexedDBStorage("tokenLists")
|
|
590
|
+
IndexedDBStorage("tokenLists"),
|
|
591
|
+
{ getOnInit: true }
|
|
543
592
|
);
|
|
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
|
+
}));
|
|
544
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
|
+
};
|
|
545
636
|
const listStateAtom = atom((get) => {
|
|
546
637
|
const memoryState = get(memoryStateAtom);
|
|
547
638
|
const value = get(loadable(tokenListsStorageAtom));
|
|
548
639
|
if (value.state === "hasData" && value.data && value.data !== EMPTY) {
|
|
549
|
-
|
|
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;
|
|
640
|
+
return mergeStoredData(memoryState, value.data);
|
|
562
641
|
}
|
|
563
642
|
return memoryState;
|
|
564
643
|
});
|
|
644
|
+
let hasHydratedActiveListUrls = false;
|
|
565
645
|
const updateListStateAtom = atom(null, (get, set, action) => __async(void 0, null, function* () {
|
|
566
|
-
const
|
|
567
|
-
|
|
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);
|
|
568
661
|
set(memoryStateAtom, __spreadValues({}, newState));
|
|
569
662
|
const tokenListsToStore = {};
|
|
570
663
|
Object.keys(newState.byUrl).forEach((url) => {
|
|
@@ -573,7 +666,7 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
573
666
|
tokenListsToStore[url] = newState.byUrl[url].current;
|
|
574
667
|
}
|
|
575
668
|
});
|
|
576
|
-
set(tokenListsStorageAtom, tokenListsToStore);
|
|
669
|
+
set(tokenListsStorageAtom, { byUrl: tokenListsToStore, activeListUrls: newState.activeListUrls });
|
|
577
670
|
}));
|
|
578
671
|
const isReadyAtom = loadable(tokenListsStorageAtom);
|
|
579
672
|
const tokenAtom = atomFamily(
|
|
@@ -657,6 +750,8 @@ var createListsAtom = (storeName, reducer, initialState) => {
|
|
|
657
750
|
updateListStateAtom,
|
|
658
751
|
tokenAtom,
|
|
659
752
|
tokenBySymbolAtom,
|
|
753
|
+
geoBlockListAtom,
|
|
754
|
+
fetchGeoBlockListAtom,
|
|
660
755
|
fetchListAtom,
|
|
661
756
|
fetchListBatchAtom,
|
|
662
757
|
useListStateReady,
|
|
@@ -842,7 +937,7 @@ function useFetchListCallback(dispatch) {
|
|
|
842
937
|
}
|
|
843
938
|
return tokenList;
|
|
844
939
|
}).catch((error) => {
|
|
845
|
-
console.
|
|
940
|
+
console.warn(`Failed to get list at url ${listUrl}`, error);
|
|
846
941
|
if (sendDispatch) {
|
|
847
942
|
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }));
|
|
848
943
|
}
|
|
@@ -854,4 +949,4 @@ function useFetchListCallback(dispatch) {
|
|
|
854
949
|
}
|
|
855
950
|
var useFetchListCallback_default = useFetchListCallback;
|
|
856
951
|
|
|
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 };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pancakeswap/token-lists",
|
|
3
|
-
"version": "0.1.
|
|
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.6.
|
|
10
|
+
"@pancakeswap/swap-sdk-core": "1.6.1"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"@reduxjs/toolkit": "^1.9.1",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"react": "^18.2.0",
|
|
24
24
|
"tsup": "^6.7.0",
|
|
25
25
|
"@types/lodash": "^4.14.182",
|
|
26
|
-
"@pancakeswap/utils": "8.0.
|
|
26
|
+
"@pancakeswap/utils": "8.0.3"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { getTokenList } from './getTokenList'
|
|
3
|
+
|
|
4
|
+
const validList = {
|
|
5
|
+
name: 'Test List',
|
|
6
|
+
timestamp: '2026-04-29T00:00:00.000Z',
|
|
7
|
+
version: {
|
|
8
|
+
major: 1,
|
|
9
|
+
minor: 0,
|
|
10
|
+
patch: 0,
|
|
11
|
+
},
|
|
12
|
+
tokens: [
|
|
13
|
+
{
|
|
14
|
+
chainId: 56,
|
|
15
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
16
|
+
decimals: 18,
|
|
17
|
+
name: 'Valid Token',
|
|
18
|
+
symbol: 'VALID',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('getTokenList', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('filters invalid tokens without warning when the list is recoverable', async () => {
|
|
29
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
30
|
+
ok: true,
|
|
31
|
+
json: async () => ({
|
|
32
|
+
...validList,
|
|
33
|
+
tokens: [
|
|
34
|
+
...validList.tokens,
|
|
35
|
+
{
|
|
36
|
+
chainId: 56,
|
|
37
|
+
address: '0x0000000000000000000000000000000000000002',
|
|
38
|
+
decimals: 18,
|
|
39
|
+
name: 'Broken Token',
|
|
40
|
+
symbol: 'BROKEN🚨',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
} as Response)
|
|
45
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
46
|
+
|
|
47
|
+
const list = await getTokenList('https://tokens.example.com/list.json')
|
|
48
|
+
|
|
49
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
50
|
+
expect(list?.tokens).toHaveLength(1)
|
|
51
|
+
expect(list?.tokens[0]?.symbol).toBe('VALID')
|
|
52
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
})
|
package/react/getTokenList.ts
CHANGED
|
@@ -17,20 +17,18 @@ export async function getTokenList(listUrl: string): Promise<TokenList | undefin
|
|
|
17
17
|
try {
|
|
18
18
|
const json = await fetchJson(url)
|
|
19
19
|
if (!validator(json)) {
|
|
20
|
-
const preFilterErrors = validator.errors
|
|
21
20
|
json.tokens = json.tokens.filter((token: any) => validator({ ...json, tokens: [token] }))
|
|
22
21
|
if (!validator(json)) {
|
|
23
22
|
const { errors } = validator
|
|
24
23
|
throw new Error(`Validation failed after filtering: ${JSON.stringify(errors)}`)
|
|
25
24
|
}
|
|
26
|
-
console.warn(`Pre-filter validation errors: ${JSON.stringify(preFilterErrors)}`)
|
|
27
25
|
}
|
|
28
26
|
return json as TokenList
|
|
29
27
|
} catch (error) {
|
|
30
28
|
// if (i === urls.length - 1) {
|
|
31
29
|
// throw new Error(`Failed to download list ${listUrl}`)
|
|
32
30
|
// }
|
|
33
|
-
console.
|
|
31
|
+
console.warn(`Failed to download or validate list from ${url}:`, error)
|
|
34
32
|
|
|
35
33
|
return undefined
|
|
36
34
|
}
|
package/react/index.test.ts
CHANGED
|
@@ -16,6 +16,9 @@ test('exports', () => {
|
|
|
16
16
|
"batchFetchTokenListFulfilled",
|
|
17
17
|
"batchFetchTokenListRejected",
|
|
18
18
|
"getTokenList",
|
|
19
|
+
"getGeoBlockTokenKey",
|
|
20
|
+
"getGeoBlockedTokenKeys",
|
|
21
|
+
"isGeoBlockedToken",
|
|
19
22
|
"findTokenByAddress",
|
|
20
23
|
"findTokenBySymbol",
|
|
21
24
|
"createListsAtom",
|
package/react/lists.ts
CHANGED
|
@@ -12,6 +12,76 @@ import {
|
|
|
12
12
|
} from './actions'
|
|
13
13
|
import { getTokenList } from './getTokenList'
|
|
14
14
|
import { ListsState } from './reducer'
|
|
15
|
+
|
|
16
|
+
export type GeoBlockList = {
|
|
17
|
+
rules?: Record<string, string[]>
|
|
18
|
+
tokens?: Record<string, string[]>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CreateListsAtomOptions = {
|
|
22
|
+
geoBlockListUrl?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Persisted payload shape. `activeListUrls` was added later - existing IndexedDB
|
|
26
|
+
// entries only have the flat `{ [url]: TokenList }` shape and must still be read.
|
|
27
|
+
type StoredListsPayload = {
|
|
28
|
+
byUrl: Record<string, any>
|
|
29
|
+
activeListUrls?: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TokenKeyLike = {
|
|
33
|
+
chainId: number
|
|
34
|
+
address: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeTokenAddress(address: string) {
|
|
38
|
+
return address.startsWith('0x') ? address.toLowerCase() : address
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getGeoBlockTokenKey(token: TokenKeyLike) {
|
|
42
|
+
return `${token.chainId}:${normalizeTokenAddress(token.address)}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeTokenKey(key: string) {
|
|
46
|
+
const separatorIndex = key.indexOf(':')
|
|
47
|
+
if (separatorIndex < 0) return key
|
|
48
|
+
const chainId = key.slice(0, separatorIndex)
|
|
49
|
+
const address = key.slice(separatorIndex + 1)
|
|
50
|
+
return `${chainId}:${normalizeTokenAddress(address)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getGeoBlockedTokenKeys(geoBlockList?: GeoBlockList, countryCode?: string) {
|
|
54
|
+
const blockedTokenKeys = new Set<string>()
|
|
55
|
+
|
|
56
|
+
if (!countryCode || !geoBlockList?.rules || !geoBlockList.tokens) {
|
|
57
|
+
return blockedTokenKeys
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const country = countryCode.toUpperCase()
|
|
61
|
+
const blockedRuleIds = new Set(
|
|
62
|
+
Object.entries(geoBlockList.rules)
|
|
63
|
+
.filter(([, countries]) => countries.includes(country))
|
|
64
|
+
.map(([ruleId]) => ruleId),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
Object.entries(geoBlockList.tokens).forEach(([tokenKey, ruleIds]) => {
|
|
68
|
+
if (ruleIds.some((ruleId) => blockedRuleIds.has(ruleId))) {
|
|
69
|
+
blockedTokenKeys.add(normalizeTokenKey(tokenKey))
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return blockedTokenKeys
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isGeoBlockedToken(token: TokenKeyLike, geoBlockList?: GeoBlockList, countryCode?: string) {
|
|
77
|
+
return getGeoBlockedTokenKeys(geoBlockList, countryCode).has(getGeoBlockTokenKey(token))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchJson(url: string): Promise<any> {
|
|
81
|
+
const res = await fetch(url)
|
|
82
|
+
if (!res.ok) throw new Error(`Failed to fetch: ${url}`)
|
|
83
|
+
return res.json()
|
|
84
|
+
}
|
|
15
85
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
16
86
|
function noop() {}
|
|
17
87
|
|
|
@@ -44,7 +114,12 @@ export function findTokenBySymbol(state: ListsState, chainId: number, symbol: st
|
|
|
44
114
|
return undefined
|
|
45
115
|
}
|
|
46
116
|
|
|
47
|
-
export const createListsAtom = (
|
|
117
|
+
export const createListsAtom = (
|
|
118
|
+
storeName: string,
|
|
119
|
+
reducer: any,
|
|
120
|
+
initialState: any,
|
|
121
|
+
options: CreateListsAtomOptions = {},
|
|
122
|
+
) => {
|
|
48
123
|
/**
|
|
49
124
|
* Persist only token lists using IndexedDB - optimized storage format
|
|
50
125
|
* @param {string} dbName - IndexedDB database name
|
|
@@ -83,51 +158,137 @@ export const createListsAtom = (storeName: string, reducer: any, initialState: a
|
|
|
83
158
|
return noopStorage
|
|
84
159
|
}
|
|
85
160
|
|
|
86
|
-
// Storage for token lists
|
|
87
|
-
|
|
161
|
+
// Storage for token lists and the user's enabled/disabled list selection.
|
|
162
|
+
// `getOnInit: true` starts the IndexedDB read as soon as this atom is created (module load)
|
|
163
|
+
// instead of waiting for the first component subscribe - by the time a consumer like the
|
|
164
|
+
// Manage Tokens modal actually mounts, the read has usually already resolved, avoiding a
|
|
165
|
+
// visible flash of the (all-enabled) default state before the persisted selection applies.
|
|
166
|
+
const tokenListsStorageAtom = atomWithStorage<StoredListsPayload | typeof EMPTY>(
|
|
88
167
|
'tokenLists',
|
|
89
168
|
EMPTY,
|
|
90
169
|
IndexedDBStorage('tokenLists'),
|
|
170
|
+
{ getOnInit: true },
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const geoBlockListStorageAtom = atomWithStorage<GeoBlockList | typeof EMPTY>(
|
|
174
|
+
'geoBlockList',
|
|
175
|
+
EMPTY,
|
|
176
|
+
IndexedDBStorage('geoBlockList'),
|
|
91
177
|
)
|
|
92
178
|
|
|
179
|
+
const geoBlockListAtom = atom<GeoBlockList | undefined>((get) => {
|
|
180
|
+
const value = get(loadable(geoBlockListStorageAtom))
|
|
181
|
+
if (value.state === 'hasData' && value.data && value.data !== EMPTY) {
|
|
182
|
+
return value.data as GeoBlockList
|
|
183
|
+
}
|
|
184
|
+
return undefined
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const fetchGeoBlockListAtom = atom<null, [], Promise<void>>(null, async (_get, set) => {
|
|
188
|
+
if (!options.geoBlockListUrl) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
set(geoBlockListStorageAtom, await fetchJson(options.geoBlockListUrl))
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn(`Failed to download geo block list from ${options.geoBlockListUrl}:`, error)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
93
199
|
// Memory state atom that holds the full ListsState
|
|
94
200
|
const memoryStateAtom = atom<ListsState>(initialState as ListsState)
|
|
95
201
|
|
|
202
|
+
// Merges a resolved storage payload's token list content and active-list selection onto a
|
|
203
|
+
// base ListsState. Shared by `listStateAtom` (read-time overlay, for display before the first
|
|
204
|
+
// dispatch has hydrated memory) and `updateListStateAtom` (one-time hydration into memory, so
|
|
205
|
+
// reducer calls - e.g. "this list's content was already fetched" - see accurate `byUrl.current`
|
|
206
|
+
// and don't treat an already-cached-but-disabled list as newly discovered and re-activate it).
|
|
207
|
+
const mergeStoredData = (baseState: ListsState, storedData: StoredListsPayload | Record<string, any>) => {
|
|
208
|
+
// Backward compat: pre-existing IndexedDB entries are a flat { [url]: TokenList } map with
|
|
209
|
+
// no `byUrl`/`activeListUrls` wrapper.
|
|
210
|
+
const storedTokenLists: Record<string, any> = storedData.byUrl ?? storedData
|
|
211
|
+
const storedActiveListUrls = Array.isArray((storedData as StoredListsPayload).activeListUrls)
|
|
212
|
+
? (storedData as StoredListsPayload).activeListUrls
|
|
213
|
+
: undefined
|
|
214
|
+
|
|
215
|
+
const mergedState = { ...baseState }
|
|
216
|
+
|
|
217
|
+
const updatedByUrl = { ...mergedState.byUrl }
|
|
218
|
+
Object.keys(storedTokenLists).forEach((url) => {
|
|
219
|
+
if (storedTokenLists[url]) {
|
|
220
|
+
updatedByUrl[url] = {
|
|
221
|
+
...updatedByUrl[url],
|
|
222
|
+
current: storedTokenLists[url],
|
|
223
|
+
// Keep existing memory state for loading, error, pendingUpdate
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
mergedState.byUrl = updatedByUrl
|
|
228
|
+
|
|
229
|
+
if (storedActiveListUrls) {
|
|
230
|
+
mergedState.activeListUrls = storedActiveListUrls
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return mergedState
|
|
234
|
+
}
|
|
235
|
+
|
|
96
236
|
const listStateAtom = atom<ListsState>((get) => {
|
|
97
237
|
// Separate this(mem/storage) is a prepare for refactor of the list part
|
|
98
238
|
const memoryState = get(memoryStateAtom)
|
|
99
239
|
const value = get(loadable(tokenListsStorageAtom))
|
|
100
240
|
|
|
101
241
|
if (value.state === 'hasData' && value.data && value.data !== EMPTY) {
|
|
102
|
-
|
|
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
|
|
242
|
+
return mergeStoredData(memoryState, value.data as StoredListsPayload | Record<string, any>)
|
|
118
243
|
}
|
|
119
244
|
|
|
120
245
|
return memoryState
|
|
121
246
|
})
|
|
122
247
|
|
|
248
|
+
// Set once the persisted storage read has resolved (whether it held data or was genuinely
|
|
249
|
+
// empty) and been merged into `memoryStateAtom` (both `byUrl.current` and `activeListUrls`).
|
|
250
|
+
// Two things depend on this:
|
|
251
|
+
// 1. Every reducer call after hydration bases itself on `memoryStateAtom` alone - a single,
|
|
252
|
+
// synchronously-consistent source of truth - instead of re-reading the async, loadable
|
|
253
|
+
// wrapped storage atom on every dispatch, which could otherwise let one action's write race
|
|
254
|
+
// another action's read of a not-yet-updated storage snapshot.
|
|
255
|
+
// 2. Until hydration completes, writes are NOT persisted (see below) - otherwise the very first
|
|
256
|
+
// action dispatched on boot (typically `updateListVersion`, fired as soon as the app
|
|
257
|
+
// considers storage "ready") can itself run before the storage read has resolved, compute a
|
|
258
|
+
// default-only state, and persist it - permanently clobbering the real persisted value the
|
|
259
|
+
// read just hadn't returned yet. Hydrating `byUrl.current` too (not just `activeListUrls`)
|
|
260
|
+
// matters because `setFulfilledTokenList` treats a null `current` as "list never loaded
|
|
261
|
+
// before" and auto re-activates it if it's in the default set - without this, refetching an
|
|
262
|
+
// already-cached-but-user-disabled list on boot would silently re-enable it.
|
|
263
|
+
let hasHydratedActiveListUrls = false
|
|
264
|
+
|
|
123
265
|
const updateListStateAtom = atom<null, any, void>(null, async (get, set, action) => {
|
|
124
|
-
const
|
|
125
|
-
|
|
266
|
+
const baseState = get(memoryStateAtom)
|
|
267
|
+
|
|
268
|
+
if (!hasHydratedActiveListUrls) {
|
|
269
|
+
const storageValue = get(loadable(tokenListsStorageAtom))
|
|
270
|
+
|
|
271
|
+
if (storageValue.state !== 'hasData') {
|
|
272
|
+
// Storage hasn't resolved yet - update memory optimistically so the UI stays responsive,
|
|
273
|
+
// but skip persisting until we know what (if anything) was already stored.
|
|
274
|
+
const optimisticState = reducer(baseState, action)
|
|
275
|
+
set(memoryStateAtom, { ...optimisticState })
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (storageValue.data && storageValue.data !== EMPTY) {
|
|
280
|
+
set(memoryStateAtom, mergeStoredData(baseState, storageValue.data as StoredListsPayload | Record<string, any>))
|
|
281
|
+
}
|
|
282
|
+
hasHydratedActiveListUrls = true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const currentState = get(memoryStateAtom)
|
|
286
|
+
const newState = reducer(currentState, action)
|
|
126
287
|
|
|
127
288
|
// Update memory state
|
|
128
289
|
set(memoryStateAtom, { ...newState })
|
|
129
290
|
|
|
130
|
-
// Extract
|
|
291
|
+
// Extract current token lists and the active list selection for storage
|
|
131
292
|
const tokenListsToStore: Record<string, any> = {}
|
|
132
293
|
Object.keys(newState.byUrl).forEach((url) => {
|
|
133
294
|
if (newState.byUrl[url]?.current) {
|
|
@@ -135,8 +296,7 @@ export const createListsAtom = (storeName: string, reducer: any, initialState: a
|
|
|
135
296
|
}
|
|
136
297
|
})
|
|
137
298
|
|
|
138
|
-
|
|
139
|
-
set(tokenListsStorageAtom, tokenListsToStore)
|
|
299
|
+
set(tokenListsStorageAtom, { byUrl: tokenListsToStore, activeListUrls: newState.activeListUrls })
|
|
140
300
|
})
|
|
141
301
|
|
|
142
302
|
const isReadyAtom = loadable(tokenListsStorageAtom)
|
|
@@ -241,6 +401,8 @@ export const createListsAtom = (storeName: string, reducer: any, initialState: a
|
|
|
241
401
|
updateListStateAtom,
|
|
242
402
|
tokenAtom,
|
|
243
403
|
tokenBySymbolAtom,
|
|
404
|
+
geoBlockListAtom,
|
|
405
|
+
fetchGeoBlockListAtom,
|
|
244
406
|
fetchListAtom,
|
|
245
407
|
fetchListBatchAtom,
|
|
246
408
|
useListStateReady,
|
|
@@ -26,7 +26,7 @@ function useFetchListCallback(
|
|
|
26
26
|
return tokenList
|
|
27
27
|
})
|
|
28
28
|
.catch((error) => {
|
|
29
|
-
console.
|
|
29
|
+
console.warn(`Failed to get list at url ${listUrl}`, error)
|
|
30
30
|
if (sendDispatch) {
|
|
31
31
|
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
|
32
32
|
}
|
package/schema/pancakeswap.json
CHANGED
|
@@ -356,6 +356,15 @@
|
|
|
356
356
|
"description": "A URI for the logo of the token list; prefer SVG or PNG of size 256x256",
|
|
357
357
|
"format": "uri",
|
|
358
358
|
"examples": ["ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM"]
|
|
359
|
+
},
|
|
360
|
+
"extensions": {
|
|
361
|
+
"type": "object",
|
|
362
|
+
"description": "An object containing arbitrary or vendor-specific token list metadata",
|
|
363
|
+
"propertyNames": {
|
|
364
|
+
"$ref": "#/definitions/ExtensionIdentifier"
|
|
365
|
+
},
|
|
366
|
+
"additionalProperties": true,
|
|
367
|
+
"maxProperties": 10
|
|
359
368
|
}
|
|
360
369
|
},
|
|
361
370
|
"if": {
|
package/src/index.test.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Token-list `extensions.scaledUIAmount` metadata for ERC-8056 (Scaled UI Amount) tokens.
|
|
2
|
+
//
|
|
3
|
+
// Curated by the indexer/CMS at ingestion time. The boolean `enabled` flag is the
|
|
4
|
+
// load-bearing FE-side check; other fields are snapshots / hints (the live multiplier
|
|
5
|
+
// is always re-read from chain via the `useScaledUIAmountMetadata` hook).
|
|
6
|
+
//
|
|
7
|
+
// See BNB Chain partner integration doc v1.0 (8 Apr 2026) §8.2.4 for the upstream
|
|
8
|
+
// shape this is modeled after.
|
|
9
|
+
|
|
10
|
+
import type { TokenInfo } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Static metadata identifying an ERC-8056 (Scaled UI Amount) token in a token list.
|
|
14
|
+
* `enabled: true` is the only field FE consumes for routing decisions; everything
|
|
15
|
+
* else is hint/snapshot data refreshed live from chain.
|
|
16
|
+
*/
|
|
17
|
+
export interface ScaledUIAmountExtension {
|
|
18
|
+
/** Set to `true` for tokens that implement ERC-8056. The load-bearing FE check. */
|
|
19
|
+
enabled: boolean
|
|
20
|
+
/** Optional snapshot of the live multiplier at the time of indexing (18-decimal fixed). */
|
|
21
|
+
multiplier?: string
|
|
22
|
+
/** Optional human-readable multiplier (e.g. `"2.0"`). Display hint only. */
|
|
23
|
+
multiplierFormatted?: string
|
|
24
|
+
/** ERC-165 interface IDs the contract claims to support. */
|
|
25
|
+
interfaceIds?: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if a token-list entry is flagged as ERC-8056 enabled by the indexer.
|
|
30
|
+
* Tokens lacking the extension or with `enabled: false` are treated as standard ERC-20s
|
|
31
|
+
* (identity multiplier `1.0×`); the FE never falls into ScaledUI math for these.
|
|
32
|
+
*
|
|
33
|
+
* Note: the load-bearing decision lives here, not on the `Token` class. Token
|
|
34
|
+
* instances stay pure identity objects per the architectural decision in the
|
|
35
|
+
* implementation doc.
|
|
36
|
+
*/
|
|
37
|
+
export function isScaledUIAmountToken(token: Pick<TokenInfo, 'extensions'>): boolean {
|
|
38
|
+
const ext = token.extensions?.scaledUIAmount
|
|
39
|
+
if (!ext || typeof ext !== 'object' || Array.isArray(ext)) return false
|
|
40
|
+
return (ext as { enabled?: unknown }).enabled === true
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
package/src/wrappedTokenInfo.ts
CHANGED
|
@@ -17,9 +17,17 @@ export interface SerializedWrappedToken extends SerializedToken {
|
|
|
17
17
|
export class WrappedTokenInfo extends Token {
|
|
18
18
|
public readonly logoURI: string | undefined
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Preserves the token-list `extensions` bag (curator-set metadata like
|
|
22
|
+
* `bridgeInfo`, `scaledUIAmount.enabled`, etc.). Stored as a readonly reference;
|
|
23
|
+
* not serialised back out of the class (kept in-memory only).
|
|
24
|
+
*/
|
|
25
|
+
public readonly extensions: TokenInfo['extensions']
|
|
26
|
+
|
|
20
27
|
constructor(tokenInfo: TokenInfo) {
|
|
21
28
|
super(tokenInfo.chainId, tokenInfo.address, tokenInfo.decimals, tokenInfo.symbol, tokenInfo.name)
|
|
22
29
|
this.logoURI = tokenInfo.logoURI
|
|
30
|
+
this.extensions = tokenInfo.extensions
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
public get serialize(): SerializedWrappedToken {
|