@anywalletkit/tokens 0.1.2 → 0.2.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/README.md +3 -3
- package/dist/index.cjs +124 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +124 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -37,10 +37,10 @@ await swaps.quoteSwap2Routes({ ...swapSource(token), toToken: 'USDC', fromTokenA
|
|
|
37
37
|
|
|
38
38
|
`inspectToken` calls the account's `getTokenInfo(address)` (`wallet-evm`, `wallet-solana`): decimals, symbol, name, supply and holder-relevant flags. A wallet, an NFT or a plain contract throws `NotATokenError`. `assessToken(info, chain)` then lists every warning, worst first:
|
|
39
39
|
|
|
40
|
-
- **danger**: `impersonation` (the symbol reads as a catalog token of the same chain, or as a native coin or major stablecoin anywhere, after folding Cyrillic / Greek look-alikes, `$` → S, `₮` → T, fullwidth and invisible characters), `permanent-delegate`, `transfer-hook`, `non-transferable`;
|
|
41
|
-
- **caution**: `unlisted` (always), `upgradeable`, `mint-authority`, `freeze-authority`, `transfer-fee`, `default-frozen`, `pausable`, `odd-characters
|
|
40
|
+
- **danger**: `impersonation` (the symbol reads as a catalog token of the same chain, or as a native coin or major stablecoin anywhere, after folding Cyrillic / Greek look-alikes, `$` → S, `₮` → T, ASCII `l` / `1` / `|` → I and `0` → O, `rn` → m, `cl` → d, `vv` → w, accents, fullwidth and invisible characters), `odd-characters` when the symbol has letters that pass for Latin ones (Cyrillic, Greek, Armenian, Cherokee, Canadian syllabics, small capitals, letterlike, enclosed and math letters) or characters that hide in text (zero-width, bidi), `permanent-delegate`, `transfer-hook`, `non-transferable`;
|
|
41
|
+
- **caution**: `unlisted` (always), `upgradeable`, `mint-authority`, `freeze-authority`, `transfer-fee`, `default-frozen`, `pausable`, `odd-characters` (emoji, CJK, accents, a space inside the symbol, odd characters in the name), `no-symbol`, `zero-supply`, `unusual-decimals`.
|
|
42
42
|
|
|
43
|
-
A user token's symbol and name are what the token says about itself: display them next to the address, never identify the token by them.
|
|
43
|
+
A user token's symbol and name are what the token says about itself: display them next to the address, never identify the token by them. Both are trimmed and cut (`tokenText`: symbol `TOKEN_SYMBOL_MAX` = 32, name `TOKEN_NAME_MAX` = 128 code points) before they are stored or assessed, so what is shown is what was checked.
|
|
44
44
|
|
|
45
45
|
## TokensTable
|
|
46
46
|
|
package/dist/index.cjs
CHANGED
|
@@ -20,13 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
TOKEN_NAME_MAX: () => TOKEN_NAME_MAX,
|
|
24
|
+
TOKEN_SYMBOL_MAX: () => TOKEN_SYMBOL_MAX,
|
|
23
25
|
TokensError: () => TokensError,
|
|
24
26
|
TokensTable: () => TokensTable,
|
|
25
27
|
assessToken: () => assessToken,
|
|
26
28
|
foldSymbol: () => foldSymbol,
|
|
27
29
|
inspectToken: () => inspectToken,
|
|
28
30
|
memoryJsonStorage: () => memoryJsonStorage,
|
|
29
|
-
swapSource: () => swapSource
|
|
31
|
+
swapSource: () => swapSource,
|
|
32
|
+
tokenText: () => tokenText
|
|
30
33
|
});
|
|
31
34
|
module.exports = __toCommonJS(src_exports);
|
|
32
35
|
|
|
@@ -43,6 +46,17 @@ var FLAG_RISKS = {
|
|
|
43
46
|
"default-frozen": { level: "caution", message: "New holders start frozen until the issuer lets them move it." },
|
|
44
47
|
"pausable": { level: "caution", message: "The issuer can pause every transfer." }
|
|
45
48
|
};
|
|
49
|
+
var TOKEN_SYMBOL_MAX = 32;
|
|
50
|
+
var TOKEN_NAME_MAX = 128;
|
|
51
|
+
function tokenText(text, max) {
|
|
52
|
+
let out = "";
|
|
53
|
+
let n = 0;
|
|
54
|
+
for (const ch of text.trim()) {
|
|
55
|
+
if (n++ >= max) break;
|
|
56
|
+
out += ch;
|
|
57
|
+
}
|
|
58
|
+
return out.trim();
|
|
59
|
+
}
|
|
46
60
|
var CONFUSABLES = {
|
|
47
61
|
"\u0410": "A",
|
|
48
62
|
"\u0412": "B",
|
|
@@ -105,13 +119,94 @@ var CONFUSABLES = {
|
|
|
105
119
|
"\u20AC": "E",
|
|
106
120
|
"\xA2": "C",
|
|
107
121
|
"\u20BF": "B",
|
|
108
|
-
"\u039E": "E"
|
|
122
|
+
"\u039E": "E",
|
|
123
|
+
// ASCII and Latin look-alikes of I and O (UTS #39 confusables): "DAl" reads as DAI, "USDl" as USD1.
|
|
124
|
+
"l": "I",
|
|
125
|
+
"1": "I",
|
|
126
|
+
"|": "I",
|
|
127
|
+
"\u0131": "I",
|
|
128
|
+
"\u0142": "I",
|
|
129
|
+
"\u01C0": "I",
|
|
130
|
+
"0": "O",
|
|
131
|
+
"\xF8": "O",
|
|
132
|
+
"\xD8": "O",
|
|
133
|
+
// Latin letters without a decomposition, and Latin-1 accented letters for runtimes without String.prototype.normalize.
|
|
134
|
+
"\xD0": "D",
|
|
135
|
+
"\xF0": "D",
|
|
136
|
+
"\u0110": "D",
|
|
137
|
+
"\u0111": "D",
|
|
138
|
+
"\u0126": "H",
|
|
139
|
+
"\u0127": "H",
|
|
140
|
+
"\u0166": "T",
|
|
141
|
+
"\u0167": "T",
|
|
142
|
+
"\xDF": "B",
|
|
143
|
+
"\xDE": "P",
|
|
144
|
+
"\xFE": "P",
|
|
145
|
+
"\u014A": "N",
|
|
146
|
+
"\u014B": "N",
|
|
147
|
+
"\xC0": "A",
|
|
148
|
+
"\xC1": "A",
|
|
149
|
+
"\xC2": "A",
|
|
150
|
+
"\xC3": "A",
|
|
151
|
+
"\xC4": "A",
|
|
152
|
+
"\xC5": "A",
|
|
153
|
+
"\xE0": "A",
|
|
154
|
+
"\xE1": "A",
|
|
155
|
+
"\xE2": "A",
|
|
156
|
+
"\xE3": "A",
|
|
157
|
+
"\xE4": "A",
|
|
158
|
+
"\xE5": "A",
|
|
159
|
+
"\xC7": "C",
|
|
160
|
+
"\xE7": "C",
|
|
161
|
+
"\xC8": "E",
|
|
162
|
+
"\xC9": "E",
|
|
163
|
+
"\xCA": "E",
|
|
164
|
+
"\xCB": "E",
|
|
165
|
+
"\xE8": "E",
|
|
166
|
+
"\xE9": "E",
|
|
167
|
+
"\xEA": "E",
|
|
168
|
+
"\xEB": "E",
|
|
169
|
+
"\xCC": "I",
|
|
170
|
+
"\xCD": "I",
|
|
171
|
+
"\xCE": "I",
|
|
172
|
+
"\xCF": "I",
|
|
173
|
+
"\xEC": "I",
|
|
174
|
+
"\xED": "I",
|
|
175
|
+
"\xEE": "I",
|
|
176
|
+
"\xEF": "I",
|
|
177
|
+
"\xD1": "N",
|
|
178
|
+
"\xF1": "N",
|
|
179
|
+
"\xD2": "O",
|
|
180
|
+
"\xD3": "O",
|
|
181
|
+
"\xD4": "O",
|
|
182
|
+
"\xD5": "O",
|
|
183
|
+
"\xD6": "O",
|
|
184
|
+
"\xF2": "O",
|
|
185
|
+
"\xF3": "O",
|
|
186
|
+
"\xF4": "O",
|
|
187
|
+
"\xF5": "O",
|
|
188
|
+
"\xF6": "O",
|
|
189
|
+
"\xD9": "U",
|
|
190
|
+
"\xDA": "U",
|
|
191
|
+
"\xDB": "U",
|
|
192
|
+
"\xDC": "U",
|
|
193
|
+
"\xF9": "U",
|
|
194
|
+
"\xFA": "U",
|
|
195
|
+
"\xFB": "U",
|
|
196
|
+
"\xFC": "U",
|
|
197
|
+
"\xDD": "Y",
|
|
198
|
+
"\xFD": "Y",
|
|
199
|
+
"\xFF": "Y"
|
|
109
200
|
};
|
|
110
|
-
var
|
|
201
|
+
var PAIRS = [[/rn/g, "m"], [/cl/g, "d"], [/vv|VV/g, "W"]];
|
|
202
|
+
var MARKS = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/g;
|
|
203
|
+
var INVISIBLE = /[\x00-\x08\x0E-\x1F\x7F-\x9F\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2800\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF9-\uFFFB]|\uDB40[\uDC00-\uDC7F\uDD00-\uDDEF]|\uD834[\uDD59\uDD73-\uDD7A]|\uD82F[\uDCA0-\uDCA3]/g;
|
|
111
204
|
function foldSymbol(text) {
|
|
112
|
-
let s = text
|
|
113
|
-
if (typeof s.normalize === "function") s = s.normalize("
|
|
205
|
+
let s = tokenText(text, TOKEN_SYMBOL_MAX).replace(INVISIBLE, "");
|
|
206
|
+
if (typeof s.normalize === "function") s = s.normalize("NFKD").replace(MARKS, "");
|
|
207
|
+
s = s.replace(/\s+/g, "");
|
|
114
208
|
s = s.replace(/[\uFF01-\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 65248));
|
|
209
|
+
for (const [re, to] of PAIRS) s = s.replace(re, to);
|
|
115
210
|
let out = "";
|
|
116
211
|
for (const ch of s) out += CONFUSABLES[ch] ?? ch;
|
|
117
212
|
return out.toUpperCase();
|
|
@@ -135,17 +230,28 @@ function lookalikeOf(folded, chain) {
|
|
|
135
230
|
return majorSymbols().get(folded);
|
|
136
231
|
}
|
|
137
232
|
var PLAIN = /^[\x20-\x7E]*$/;
|
|
233
|
+
var LOOKALIKE_LETTERS = /[\u0250-\u02FF\u0370-\u03FF\u0400-\u052F\u0530-\u058F\u10B9\u10BD\u10C7\u10CD\u10E7\u10FD\u10FF\u13A0-\u13FF\u1400-\u167F\u16A0-\u16FF\u1C80-\u1C8F\u1C95\u1CB7\u1CBD\u1CBF\u1D00-\u1DBF\u1F00-\u1FFF\u2100-\u2116\u2118-\u211F\u2121\u2123-\u214F\u2160-\u2188\u2460-\u24FF\u2C80-\u2CFF\u2D2D\u2D30-\u2D7F\u2DE0-\u2DFF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA720-\uA7FF\uAB30-\uABBF]|\uD835[\uDC00-\uDFFF]|\uD83C[\uDD10-\uDD69]|\uD801[\uDC00-\uDC4F\uDCB0-\uDCFF]|\uD800[\uDE80-\uDEDF\uDF00-\uDF2F]|\uD806[\uDCA0-\uDCFF]/;
|
|
234
|
+
var HIDING = /[\x00-\x08\x0E-\x1F\x7F-\x9F\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B\u200C\u200E\u200F\u202A-\u202E\u2060-\u206F\u2800\u3164\uFEFF\uFFA0\uFFF9-\uFFFB]|\uDB40[\uDC00-\uDC7F\uDD00-\uDDEF]|\uD834[\uDD59\uDD73-\uDD7A]|\uD82F[\uDCA0-\uDCA3]/;
|
|
235
|
+
var LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
236
|
+
var EMOJI_TAG_SEQUENCE = /\uD83C\uDFF4(?:\uDB40[\uDC20-\uDC7E])+\uDB40\uDC7F/g;
|
|
237
|
+
var SPELLING_ZWNJ = /([^\x00-\x7F])\u200C(?=[^\x00-\x7F])/g;
|
|
238
|
+
var passesOrHides = (text) => LOOKALIKE_LETTERS.test(text) || HIDING.test(text.replace(EMOJI_TAG_SEQUENCE, "").replace(SPELLING_ZWNJ, "$1"));
|
|
138
239
|
function assessToken(info, chain) {
|
|
139
240
|
const risks = [{ code: "unlisted", level: "caution", message: "Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself." }];
|
|
140
|
-
const
|
|
141
|
-
const
|
|
241
|
+
const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX);
|
|
242
|
+
const name = tokenText(info.name, TOKEN_NAME_MAX);
|
|
243
|
+
const whole = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX).replace(/\s+/g, "");
|
|
244
|
+
const lookalike = lookalikeOf(foldSymbol(symbol), chain) ?? lookalikeOf(foldSymbol(whole), chain);
|
|
142
245
|
if (lookalike) {
|
|
143
|
-
risks.push({ code: "impersonation", level: "danger", message: `It calls itself ${JSON.stringify(
|
|
246
|
+
risks.push({ code: "impersonation", level: "danger", message: `It calls itself ${JSON.stringify(symbol)}, like ${lookalike}, but it is a different token at a different address.` });
|
|
144
247
|
}
|
|
145
|
-
|
|
146
|
-
|
|
248
|
+
const head = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX);
|
|
249
|
+
if (passesOrHides(head) || passesOrHides(symbol) || LINE_BREAK.test(symbol)) {
|
|
250
|
+
risks.push({ code: "odd-characters", level: "danger", message: "Its symbol uses letters of another alphabet that pass for Latin ones (Cyrillic, Greek\u2026) or characters that hide in text." });
|
|
251
|
+
} else if (!PLAIN.test(symbol) || /\s/.test(symbol) || !PLAIN.test(name) || name !== name.replace(INVISIBLE, "")) {
|
|
252
|
+
risks.push({ code: "odd-characters", level: "caution", message: "Its symbol or name uses unusual characters (emoji, other scripts, accents), spaces inside the symbol, or invisible ones." });
|
|
147
253
|
}
|
|
148
|
-
if (!
|
|
254
|
+
if (!symbol) risks.push({ code: "no-symbol", level: "caution", message: "The token reports no symbol." });
|
|
149
255
|
if (info.totalSupply === 0n) risks.push({ code: "zero-supply", level: "caution", message: "Nothing has been minted." });
|
|
150
256
|
if (info.decimals > 24) risks.push({ code: "unusual-decimals", level: "caution", message: `It uses ${info.decimals} decimals; amounts may display oddly.` });
|
|
151
257
|
for (const flag of info.flags) {
|
|
@@ -191,8 +297,8 @@ function validEntry(t) {
|
|
|
191
297
|
return {
|
|
192
298
|
chain: e.chain,
|
|
193
299
|
address: e.address,
|
|
194
|
-
symbol: e.symbol
|
|
195
|
-
name: e.name
|
|
300
|
+
symbol: tokenText(e.symbol, TOKEN_SYMBOL_MAX),
|
|
301
|
+
name: tokenText(e.name, TOKEN_NAME_MAX),
|
|
196
302
|
decimals: e.decimals,
|
|
197
303
|
standard: e.standard,
|
|
198
304
|
flags: [...e.flags],
|
|
@@ -213,17 +319,19 @@ async function inspectToken(account, address, now = Date.now) {
|
|
|
213
319
|
if (listed) return { listed };
|
|
214
320
|
if (typeof account.getTokenInfo !== "function") throw new TokensError("unsupported-chain", `This ${account.chain} account cannot read token info.`);
|
|
215
321
|
const info = await account.getTokenInfo(ref);
|
|
322
|
+
const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX);
|
|
323
|
+
const name = tokenText(info.name, TOKEN_NAME_MAX);
|
|
216
324
|
const token = {
|
|
217
325
|
chain,
|
|
218
326
|
address: info.address,
|
|
219
|
-
symbol
|
|
220
|
-
name
|
|
327
|
+
symbol,
|
|
328
|
+
name,
|
|
221
329
|
decimals: info.decimals,
|
|
222
330
|
standard: info.standard,
|
|
223
331
|
flags: [...info.flags],
|
|
224
332
|
addedAt: now()
|
|
225
333
|
};
|
|
226
|
-
return { info, assessment: assessToken(info, chain), token };
|
|
334
|
+
return { info, assessment: assessToken({ ...info, symbol, name }, chain), token };
|
|
227
335
|
}
|
|
228
336
|
function swapSource(token) {
|
|
229
337
|
return { fromToken: token.address, fromTokenDecimals: token.decimals };
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/assess.ts","../src/table.ts"],"sourcesContent":["export { assessToken, foldSymbol, type TokenAssessment, type TokenRisk, type TokenRiskCode, type TokenRiskLevel } from './assess.js'\nexport {\n TokensTable, TokensError, inspectToken, swapSource, memoryJsonStorage,\n type JsonStorage, type TokensState, type UserToken, type TokenInspection, type TokensErrorCode,\n} from './table.js'\n","import type { TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { CC_SOURCE_CHAINS, catalogChainId, ccChain } from '@anywalletkit/swap2-assets'\n\n/** `danger`: the token can take or trap the user's money, or pretends to be another one. `caution`: worth knowing. */\nexport type TokenRiskLevel = 'danger' | 'caution'\n\n/**\n * Why a token deserves a warning:\n * - `unlisted`: always — the wallet's list does not include it, and anyone can deploy a token with any name and logo.\n * - `impersonation`: its symbol reads as a known token's (USDC, ETH, …), look-alike letters included.\n * - `odd-characters`: its symbol or name uses characters outside plain ASCII, or invisible ones.\n * - `no-symbol`: it reports no symbol.\n * - `zero-supply`: nothing has been minted.\n * - `unusual-decimals`: more than 24 decimals.\n * - a {@link TokenFlag} read from the chain (issuer powers, Token-2022 extensions).\n */\nexport type TokenRiskCode = 'unlisted' | 'impersonation' | 'odd-characters' | 'no-symbol' | 'zero-supply' | 'unusual-decimals' | TokenFlag\n\n/** One warning, worded for the person adding the token. */\nexport interface TokenRisk {\n code: TokenRiskCode\n level: TokenRiskLevel\n message: string\n}\n\n/** Every warning that applies, worst first, and the worst level. A user token is never \"safe\": `caution` is the floor. */\nexport interface TokenAssessment {\n level: TokenRiskLevel\n risks: TokenRisk[]\n}\n\nconst FLAG_RISKS: Readonly<Record<TokenFlag, { level: TokenRiskLevel; message: string }>> = {\n 'upgradeable': { level: 'caution', message: \"The token's owner can replace its code.\" },\n 'mint-authority': { level: 'caution', message: 'The issuer can mint more at any time.' },\n 'freeze-authority': { level: 'caution', message: 'The issuer can freeze your balance.' },\n 'permanent-delegate': { level: 'danger', message: 'The issuer can move or burn the tokens in your wallet.' },\n 'transfer-fee': { level: 'caution', message: 'Every transfer pays a fee to the issuer.' },\n 'transfer-hook': { level: 'danger', message: \"A program of the issuer's choosing runs on every transfer: it can block selling.\" },\n 'non-transferable': { level: 'danger', message: 'It cannot be moved: not sent, not swapped.' },\n 'default-frozen': { level: 'caution', message: 'New holders start frozen until the issuer lets them move it.' },\n 'pausable': { level: 'caution', message: 'The issuer can pause every transfer.' },\n}\n\n/** Letters that render like Latin capitals (Cyrillic, Greek) and currency signs used as letters. */\nconst CONFUSABLES: Readonly<Record<string, string>> = {\n 'А': 'A', 'В': 'B', 'Е': 'E', 'К': 'K', 'М': 'M', 'Н': 'H', 'О': 'O', 'Р': 'P', 'С': 'C', 'Т': 'T', 'Х': 'X', 'У': 'Y', 'І': 'I', 'Ј': 'J', 'Ѕ': 'S', 'Ԁ': 'D',\n 'а': 'A', 'в': 'B', 'е': 'E', 'к': 'K', 'м': 'M', 'н': 'H', 'о': 'O', 'р': 'P', 'с': 'C', 'т': 'T', 'х': 'X', 'у': 'Y', 'і': 'I', 'ј': 'J', 'ѕ': 'S', 'ԁ': 'D',\n 'Α': 'A', 'Β': 'B', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Ι': 'I', 'Κ': 'K', 'Μ': 'M', 'Ν': 'N', 'Ο': 'O', 'Ρ': 'P', 'Τ': 'T', 'Υ': 'Y', 'Χ': 'X',\n 'α': 'A', 'ε': 'E', 'ι': 'I', 'κ': 'K', 'ν': 'V', 'ο': 'O', 'ρ': 'P', 'τ': 'T', 'υ': 'U', 'χ': 'X',\n '$': 'S', '₮': 'T', '€': 'E', '¢': 'C', '₿': 'B', 'Ξ': 'E',\n}\n/** Zero-width, bidi and other invisible format characters. */\nconst INVISIBLE = /[͏ᅟᅠ឴឵᠋-᠏---ㅤ︀-️ᅠ]/g\n\n/** A symbol folded the way a reader sees it: invisible characters gone, compatibility forms unified, look-alikes mapped, upper case. */\nexport function foldSymbol(text: string): string {\n let s = text.replace(INVISIBLE, '').replace(/\\s+/g, '')\n if (typeof s.normalize === 'function') s = s.normalize('NFKC')\n // Fullwidth ASCII by hand too: some Hermes builds ship without String.prototype.normalize.\n s = s.replace(/[\\uFF01-\\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))\n let out = ''\n for (const ch of s) out += CONFUSABLES[ch] ?? ch\n return out.toUpperCase()\n}\n\n/** Symbols nobody should reuse on any chain: the big stablecoins and wrapped coins (natives are added from the catalog). */\nconst MAJOR_TOKENS = ['USDT', 'USDC', 'DAI', 'USDe', 'USDS', 'PYUSD', 'FDUSD', 'USD1', 'USDG', 'EURC', 'WETH', 'WBTC', 'cbBTC', 'stETH', 'wstETH', 'WSOL']\n\nlet majors: ReadonlyMap<string, string> | undefined\n/** Folded → display: every chain's native coin from the catalog, plus {@link MAJOR_TOKENS}. */\nfunction majorSymbols(): ReadonlyMap<string, string> {\n if (majors) return majors\n const m = new Map<string, string>()\n for (const chain of CC_SOURCE_CHAINS) for (const a of chain.assets) if (!a.contract && !m.has(foldSymbol(a.symbol))) m.set(foldSymbol(a.symbol), a.symbol)\n for (const sym of MAJOR_TOKENS) if (!m.has(foldSymbol(sym))) m.set(foldSymbol(sym), sym)\n majors = m\n return m\n}\n\n/** The known token a folded symbol reads as: any catalog token on the same chain, or a major one anywhere. */\nfunction lookalikeOf(folded: string, chain: string | undefined): string | undefined {\n if (!folded) return undefined\n if (chain) {\n const same = ccChain(catalogChainId(chain))?.assets.find((a) => foldSymbol(a.symbol) === folded)\n if (same) return same.symbol\n }\n return majorSymbols().get(folded)\n}\n\nconst PLAIN = /^[\\x20-\\x7E]*$/\n\n/**\n * Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.\n * Pure: nothing is fetched. `impersonation` when its symbol (look-alike letters folded) reads as a\n * catalog token on the same `chain` — the real one would be in the list, this is another contract —\n * or as a native coin or major stablecoin anywhere. A bridged token of another chain's list (JUP on\n * Solana, listed on Unichain) is not flagged.\n */\nexport function assessToken(info: TokenInfo, chain?: string): TokenAssessment {\n const risks: TokenRisk[] = [{ code: 'unlisted', level: 'caution', message: \"Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself.\" }]\n const folded = foldSymbol(info.symbol)\n const lookalike = lookalikeOf(folded, chain)\n if (lookalike) {\n risks.push({ code: 'impersonation', level: 'danger', message: `It calls itself ${JSON.stringify(info.symbol)}, like ${lookalike}, but it is a different token at a different address.` })\n }\n if (!PLAIN.test(info.symbol) || !PLAIN.test(info.name) || info.symbol !== info.symbol.replace(INVISIBLE, '') || info.name !== info.name.replace(INVISIBLE, '')) {\n risks.push({ code: 'odd-characters', level: 'caution', message: 'Its symbol or name uses characters that can look like other letters, or invisible ones.' })\n }\n if (!info.symbol.trim()) risks.push({ code: 'no-symbol', level: 'caution', message: 'The token reports no symbol.' })\n if (info.totalSupply === 0n) risks.push({ code: 'zero-supply', level: 'caution', message: 'Nothing has been minted.' })\n if (info.decimals > 24) risks.push({ code: 'unusual-decimals', level: 'caution', message: `It uses ${info.decimals} decimals; amounts may display oddly.` })\n for (const flag of info.flags) {\n const r = FLAG_RISKS[flag]\n if (r) risks.push({ code: flag, ...r })\n }\n risks.sort((a, b) => (a.level === b.level ? 0 : a.level === 'danger' ? -1 : 1))\n return { level: risks.some((r) => r.level === 'danger') ? 'danger' : 'caution', risks }\n}\n","import type { IWalletAccountReadOnly, TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { catalogChainId, ccAssetByToken, isTokenAddress, type CcAsset } from '@anywalletkit/swap2-assets'\nimport { assessToken, type TokenAssessment } from './assess.js'\n\n/** One JSON value under one key — the shape every small store needs (AsyncStorage, chrome.storage, memory). */\nexport interface JsonStorage<T> {\n /** The stored value, or `null` on first run. */\n get(): Promise<T | null>\n /** Persist the value. */\n set(value: T): Promise<void>\n}\n\n/** In-memory storage for tests and smoke runs. */\nexport function memoryJsonStorage<T>(initial: T | null = null): JsonStorage<T> & { value: T | null } {\n const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T\n const s = {\n value: initial,\n async get() { return s.value === null ? null : clone(s.value) },\n async set(value: T) { s.value = clone(value) },\n }\n return s\n}\n\n/** A token the user added. `symbol` and `name` are what the token reports: display them, never match on them. */\nexport interface UserToken {\n /** CAIP-2 chain id, the catalog's key (`solana:mainnet`, `eip155:1`). */\n chain: string\n /** Contract (EVM, checksummed as read) or mint (Solana). The token's identity. */\n address: string\n symbol: string\n name: string\n decimals: number\n /** `erc20`, `spl` or `spl-token-2022`. */\n standard: string\n /** Facts read from the chain when it was added. */\n flags: TokenFlag[]\n /** When the user added it (ms since epoch). */\n addedAt: number\n}\n\n/** What {@link TokensTable} stores. */\nexport interface TokensState {\n v: 1\n tokens: UserToken[]\n}\n\n/** Codes of {@link TokensError}. */\nexport type TokensErrorCode = 'not-loaded' | 'unsupported-chain' | 'bad-address' | 'listed' | 'bad-token' | 'full'\n\n/** A refusal from the tokens table or {@link inspectToken}, with a machine-readable `code`. */\nexport class TokensError extends Error {\n readonly code: TokensErrorCode\n constructor(code: TokensErrorCode, message: string) {\n super(message)\n this.name = 'TokensError'\n this.code = code\n }\n}\n\nconst sameAddress = (chain: string, a: string, b: string) => (chain.startsWith('eip155:') ? a.toLowerCase() === b.toLowerCase() : a === b)\nconst FLAGS: ReadonlySet<string> = new Set<TokenFlag>(['upgradeable', 'mint-authority', 'freeze-authority', 'permanent-delegate', 'transfer-fee', 'transfer-hook', 'non-transferable', 'default-frozen', 'pausable'])\n\n/** A stored entry as it must be, or null (a hand-edited or corrupted entry is dropped, never trusted). */\nfunction validEntry(t: unknown): UserToken | null {\n if (!t || typeof t !== 'object') return null\n const e = t as Record<string, unknown>\n if (typeof e.chain !== 'string' || typeof e.address !== 'string' || !isTokenAddress(e.chain, e.address)) return null\n if (typeof e.decimals !== 'number' || !Number.isInteger(e.decimals) || e.decimals < 0 || e.decimals > 77) return null\n if (typeof e.symbol !== 'string' || typeof e.name !== 'string' || typeof e.standard !== 'string' || typeof e.addedAt !== 'number') return null\n if (!Array.isArray(e.flags) || !e.flags.every((f) => typeof f === 'string' && FLAGS.has(f))) return null\n return {\n chain: e.chain, address: e.address, symbol: e.symbol.slice(0, 128), name: e.name.slice(0, 128), decimals: e.decimals,\n standard: e.standard, flags: [...e.flags] as TokenFlag[], addedAt: e.addedAt,\n }\n}\n\n/** The result of looking a token up before adding it. */\nexport type TokenInspection =\n | { listed: CcAsset }\n | { listed?: undefined; info: TokenInfo; assessment: TokenAssessment; token: UserToken }\n\n/**\n * Read a token from the chain through `account` (its chain decides where) and assess it. A token the\n * catalog already lists comes back as `{ listed }`: nothing to add. Otherwise the token, ready for\n * {@link TokensTable.add}, and the warnings to show first. Throws {@link TokensError}\n * (`unsupported-chain`, `bad-address`) or the chain package's `NotATokenError`.\n */\nexport async function inspectToken(\n account: Pick<IWalletAccountReadOnly, 'chain' | 'getTokenInfo'> & { getChainId?: () => Promise<number | bigint> },\n address: string,\n now: () => number = Date.now,\n): Promise<TokenInspection> {\n let reported = account.chain\n // An EVM account created without a chain id reports 'eip155:unknown' until it has asked its RPC:\n // ask now, or the catalog check (already listed? a look-alike of this chain's tokens?) would miss.\n if (reported === 'eip155:unknown') {\n if (typeof account.getChainId !== 'function') throw new TokensError('unsupported-chain', 'The account does not know its chain id; create it with chainId.')\n reported = `eip155:${String(await account.getChainId())}`\n }\n const chain = catalogChainId(reported)\n if (!chain.startsWith('eip155:') && !chain.startsWith('solana:')) throw new TokensError('unsupported-chain', `Adding tokens is supported on EVM chains and Solana, not ${account.chain}.`)\n const ref = address.trim()\n if (!isTokenAddress(chain, ref)) throw new TokensError('bad-address', `${JSON.stringify(address)} is not a token address on ${chain}.`)\n const listed = ccAssetByToken(chain, ref)\n if (listed) return { listed }\n if (typeof account.getTokenInfo !== 'function') throw new TokensError('unsupported-chain', `This ${account.chain} account cannot read token info.`)\n const info = await account.getTokenInfo(ref)\n const token: UserToken = {\n chain, address: info.address, symbol: info.symbol.slice(0, 128), name: info.name.slice(0, 128), decimals: info.decimals,\n standard: info.standard, flags: [...info.flags], addedAt: now(),\n }\n return { info, assessment: assessToken(info, chain), token }\n}\n\n/**\n * The swap options that name a user token as the source: its address and decimals. Pass them to an\n * engine created with `allowUnlistedTokens: true`.\n */\nexport function swapSource(token: Pick<UserToken, 'address' | 'decimals'>): { fromToken: string; fromTokenDecimals: number } {\n return { fromToken: token.address, fromTokenDecimals: token.decimals }\n}\n\n/**\n * The user's own tokens, over any JSON storage: load once at cold start, then read synchronously.\n * Writes queue behind each other; a failed save leaves memory as stored. Tokens the catalog lists are\n * refused (they are already there), and so are addresses that are not EVM contracts or Solana mints.\n */\nexport class TokensTable {\n private _state: TokensState | null = null\n private _loading: Promise<TokensState> | null = null\n private _writes: Promise<unknown> = Promise.resolve()\n private readonly listeners = new Set<() => void>()\n readonly maxTokens: number\n\n constructor(private readonly storage: JsonStorage<TokensState>, options: { maxTokens?: number } = {}) {\n this.maxTokens = options.maxTokens ?? 200\n }\n\n /** Read the stored list. Call once before the sync reads; a failed read throws and leaves the table unloaded. */\n async load(): Promise<void> {\n await this.state()\n }\n\n /** Called after every successful write. Returns the unsubscribe function. */\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => { this.listeners.delete(fn) }\n }\n\n /** The user's tokens, oldest first; only those on `chain` when given. */\n list(chain?: string): UserToken[] {\n const s = this.loaded()\n const want = chain === undefined ? undefined : catalogChainId(chain)\n return s.tokens.filter((t) => want === undefined || t.chain === want).map((t) => ({ ...t, flags: [...t.flags] }))\n }\n\n /** The user token at `address` on `chain`, if added. */\n get(chain: string, address: string): UserToken | undefined {\n const c = catalogChainId(chain)\n const t = this.loaded().tokens.find((x) => x.chain === c && sameAddress(c, x.address, address))\n return t ? { ...t, flags: [...t.flags] } : undefined\n }\n\n /** Add (or refresh) a token — usually `inspection.token` after the user accepted the warnings. */\n async add(token: UserToken): Promise<UserToken> {\n const entry = validEntry({ ...token, chain: catalogChainId(token.chain) })\n if (!entry) throw new TokensError('bad-token', 'Not a valid token entry (address, decimals 0–77, symbol, name, standard, flags).')\n if (ccAssetByToken(entry.chain, entry.address)) throw new TokensError('listed', `${entry.address} is already in the wallet's token list.`)\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === entry.chain && sameAddress(entry.chain, t.address, entry.address)))\n if (rest.length >= this.maxTokens) throw new TokensError('full', `At most ${this.maxTokens} tokens can be added.`)\n return { v: 1, tokens: [...rest, entry] }\n })\n return { ...entry, flags: [...entry.flags] }\n }\n\n /** Remove a token. Returns whether it was there. */\n async remove(chain: string, address: string): Promise<boolean> {\n const c = catalogChainId(chain)\n let removed = false\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === c && sameAddress(c, t.address, address)))\n removed = rest.length !== s.tokens.length\n return { v: 1, tokens: rest }\n })\n return removed\n }\n\n private loaded(): TokensState {\n if (!this._state) throw new TokensError('not-loaded', 'Call load() before reading the tokens table.')\n return this._state\n }\n\n private async state(): Promise<TokensState> {\n if (this._state) return this._state\n this._loading ??= (async () => {\n let stored: TokensState | null\n try {\n stored = await this.storage.get()\n } catch (e) {\n // A failed read is not an empty list: the next write would erase the user's tokens.\n this._loading = null\n throw e\n }\n const tokens = stored && stored.v === 1 && Array.isArray(stored.tokens)\n ? stored.tokens.map(validEntry).filter((t): t is UserToken => t !== null)\n : []\n this._state = { v: 1, tokens }\n return this._state\n })()\n return this._loading\n }\n\n private update(change: (s: TokensState) => TokensState): Promise<void> {\n const run = this._writes.then(async () => {\n const next = change(await this.state())\n await this.storage.set(next)\n this._state = next\n for (const fn of this.listeners) {\n try { fn() } catch { /* a listener's failure is its own */ }\n }\n })\n this._writes = run.catch(() => undefined)\n return run\n }\n}\n"],"mappings":"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","names":["import_swap2_assets"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/assess.ts","../src/table.ts"],"sourcesContent":["export { TOKEN_NAME_MAX, TOKEN_SYMBOL_MAX, assessToken, foldSymbol, tokenText, type TokenAssessment, type TokenRisk, type TokenRiskCode, type TokenRiskLevel } from './assess.js'\nexport {\n TokensTable, TokensError, inspectToken, swapSource, memoryJsonStorage,\n type JsonStorage, type TokensState, type UserToken, type TokenInspection, type TokensErrorCode,\n} from './table.js'\n","import type { TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { CC_SOURCE_CHAINS, catalogChainId, ccChain } from '@anywalletkit/swap2-assets'\n\n/** `danger`: the token can take or trap the user's money, or pretends to be another one. `caution`: worth knowing. */\nexport type TokenRiskLevel = 'danger' | 'caution'\n\n/**\n * Why a token deserves a warning:\n * - `unlisted`: always — the wallet's list does not include it, and anyone can deploy a token with any name and logo.\n * - `impersonation`: its symbol reads as a known token's (USDC, ETH, …), look-alike letters included.\n * - `odd-characters`: its symbol or name uses characters outside plain ASCII, or invisible ones.\n * - `no-symbol`: it reports no symbol.\n * - `zero-supply`: nothing has been minted.\n * - `unusual-decimals`: more than 24 decimals.\n * - a {@link TokenFlag} read from the chain (issuer powers, Token-2022 extensions).\n */\nexport type TokenRiskCode = 'unlisted' | 'impersonation' | 'odd-characters' | 'no-symbol' | 'zero-supply' | 'unusual-decimals' | TokenFlag\n\n/** One warning, worded for the person adding the token. */\nexport interface TokenRisk {\n code: TokenRiskCode\n level: TokenRiskLevel\n message: string\n}\n\n/** Every warning that applies, worst first, and the worst level. A user token is never \"safe\": `caution` is the floor. */\nexport interface TokenAssessment {\n level: TokenRiskLevel\n risks: TokenRisk[]\n}\n\nconst FLAG_RISKS: Readonly<Record<TokenFlag, { level: TokenRiskLevel; message: string }>> = {\n 'upgradeable': { level: 'caution', message: \"The token's owner can replace its code.\" },\n 'mint-authority': { level: 'caution', message: 'The issuer can mint more at any time.' },\n 'freeze-authority': { level: 'caution', message: 'The issuer can freeze your balance.' },\n 'permanent-delegate': { level: 'danger', message: 'The issuer can move or burn the tokens in your wallet.' },\n 'transfer-fee': { level: 'caution', message: 'Every transfer pays a fee to the issuer.' },\n 'transfer-hook': { level: 'danger', message: \"A program of the issuer's choosing runs on every transfer: it can block selling.\" },\n 'non-transferable': { level: 'danger', message: 'It cannot be moved: not sent, not swapped.' },\n 'default-frozen': { level: 'caution', message: 'New holders start frozen until the issuer lets them move it.' },\n 'pausable': { level: 'caution', message: 'The issuer can pause every transfer.' },\n}\n\n/** Longest symbol the wallet stores and assesses (code points); a longer one is cut, after trimming, before anything reads it. */\nexport const TOKEN_SYMBOL_MAX = 32\n/** Longest name the wallet stores (code points). */\nexport const TOKEN_NAME_MAX = 128\n\n/**\n * A symbol or name as the wallet keeps and shows it: trimmed, cut to `max` code points, trimmed again —\n * so \"USDC\" + 124 spaces + \"X\" is stored, shown and assessed as \"USDC\" (scan 2026-10-27 modules, F28).\n */\nexport function tokenText(text: string, max: number): string {\n let out = ''\n let n = 0\n for (const ch of text.trim()) {\n if (n++ >= max) break\n out += ch\n }\n return out.trim()\n}\n\n/** Letters that render like Latin capitals (Cyrillic, Greek) and currency signs used as letters. */\nconst CONFUSABLES: Readonly<Record<string, string>> = {\n 'А': 'A', 'В': 'B', 'Е': 'E', 'К': 'K', 'М': 'M', 'Н': 'H', 'О': 'O', 'Р': 'P', 'С': 'C', 'Т': 'T', 'Х': 'X', 'У': 'Y', 'І': 'I', 'Ј': 'J', 'Ѕ': 'S', 'Ԁ': 'D',\n 'а': 'A', 'в': 'B', 'е': 'E', 'к': 'K', 'м': 'M', 'н': 'H', 'о': 'O', 'р': 'P', 'с': 'C', 'т': 'T', 'х': 'X', 'у': 'Y', 'і': 'I', 'ј': 'J', 'ѕ': 'S', 'ԁ': 'D',\n 'Α': 'A', 'Β': 'B', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Ι': 'I', 'Κ': 'K', 'Μ': 'M', 'Ν': 'N', 'Ο': 'O', 'Ρ': 'P', 'Τ': 'T', 'Υ': 'Y', 'Χ': 'X',\n 'α': 'A', 'ε': 'E', 'ι': 'I', 'κ': 'K', 'ν': 'V', 'ο': 'O', 'ρ': 'P', 'τ': 'T', 'υ': 'U', 'χ': 'X',\n '$': 'S', '₮': 'T', '€': 'E', '¢': 'C', '₿': 'B', 'Ξ': 'E',\n // ASCII and Latin look-alikes of I and O (UTS #39 confusables): \"DAl\" reads as DAI, \"USDl\" as USD1.\n 'l': 'I', '1': 'I', '|': 'I', 'ı': 'I', 'ł': 'I', 'ǀ': 'I', '0': 'O', 'ø': 'O', 'Ø': 'O',\n // Latin letters without a decomposition, and Latin-1 accented letters for runtimes without String.prototype.normalize.\n 'Ð': 'D', 'ð': 'D', 'Đ': 'D', 'đ': 'D', 'Ħ': 'H', 'ħ': 'H', 'Ŧ': 'T', 'ŧ': 'T', 'ß': 'B', 'Þ': 'P', 'þ': 'P', 'Ŋ': 'N', 'ŋ': 'N',\n 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'à': 'A', 'á': 'A', 'â': 'A', 'ã': 'A', 'ä': 'A', 'å': 'A',\n 'Ç': 'C', 'ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'è': 'E', 'é': 'E', 'ê': 'E', 'ë': 'E',\n 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I', 'ì': 'I', 'í': 'I', 'î': 'I', 'ï': 'I', 'Ñ': 'N', 'ñ': 'N',\n 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 'O', 'ò': 'O', 'ó': 'O', 'ô': 'O', 'õ': 'O', 'ö': 'O',\n 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'ù': 'U', 'ú': 'U', 'û': 'U', 'ü': 'U', 'Ý': 'Y', 'ý': 'Y', 'ÿ': 'Y',\n}\n/** Letter pairs that read as one letter (UTS #39): rn → m, cl → d, vv → w. */\nconst PAIRS: ReadonlyArray<[RegExp, string]> = [[/rn/g, 'm'], [/cl/g, 'd'], [/vv|VV/g, 'W']]\n/** Combining marks: an accent over a Latin letter reads as the letter. */\nconst MARKS = /[\\u0300-\\u036f\\u1ab0-\\u1aff\\u1dc0-\\u1dff\\u20d0-\\u20ff\\ufe20-\\ufe2f]/g\n/**\n * Characters a reader does not see: zero-width, bidi and other format characters, C0 / C1 controls,\n * fillers and blank glyphs (Hangul fillers, U+2800 braille blank), interlinear annotation, and the\n * astral ones — tag characters (ASCII smuggling), variation selectors 17–256, musical and shorthand\n * format controls. Explicit ranges: Hermes has no Unicode property escapes.\n */\nconst INVISIBLE = /[\\x00-\\x08\\x0E-\\x1F\\x7F-\\x9F\\u00AD\\u034F\\u061C\\u115F\\u1160\\u17B4\\u17B5\\u180B-\\u180F\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u2800\\u3164\\uFE00-\\uFE0F\\uFEFF\\uFFA0\\uFFF9-\\uFFFB]|\\uDB40[\\uDC00-\\uDC7F\\uDD00-\\uDDEF]|\\uD834[\\uDD59\\uDD73-\\uDD7A]|\\uD82F[\\uDCA0-\\uDCA3]/g\n\n/**\n * A symbol folded the way a reader sees it — a confusables skeleton in the spirit of UTS #39: invisible\n * characters and spaces gone, compatibility forms unified and accents dropped (NFKD), look-alike letters\n * mapped (Cyrillic, Greek, currency signs, and ASCII l / 1 / | → I, 0 → O; rn → m, cl → d, vv → w), upper\n * case. Only the first {@link TOKEN_SYMBOL_MAX} code points are read (scan 2026-10-27 modules, F36).\n */\nexport function foldSymbol(text: string): string {\n let s = tokenText(text, TOKEN_SYMBOL_MAX).replace(INVISIBLE, '')\n if (typeof s.normalize === 'function') s = s.normalize('NFKD').replace(MARKS, '')\n // After NFKD: a spacing accent (´ ¨ ¸ ‾) decomposes into a space and a mark.\n s = s.replace(/\\s+/g, '')\n // Fullwidth ASCII by hand too: some Hermes builds ship without String.prototype.normalize.\n s = s.replace(/[\\uFF01-\\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))\n for (const [re, to] of PAIRS) s = s.replace(re, to)\n let out = ''\n for (const ch of s) out += CONFUSABLES[ch] ?? ch\n return out.toUpperCase()\n}\n\n/** Symbols nobody should reuse on any chain: the big stablecoins and wrapped coins (natives are added from the catalog). */\nconst MAJOR_TOKENS = ['USDT', 'USDC', 'DAI', 'USDe', 'USDS', 'PYUSD', 'FDUSD', 'USD1', 'USDG', 'EURC', 'WETH', 'WBTC', 'cbBTC', 'stETH', 'wstETH', 'WSOL']\n\nlet majors: ReadonlyMap<string, string> | undefined\n/** Folded → display: every chain's native coin from the catalog, plus {@link MAJOR_TOKENS}. */\nfunction majorSymbols(): ReadonlyMap<string, string> {\n if (majors) return majors\n const m = new Map<string, string>()\n for (const chain of CC_SOURCE_CHAINS) for (const a of chain.assets) if (!a.contract && !m.has(foldSymbol(a.symbol))) m.set(foldSymbol(a.symbol), a.symbol)\n for (const sym of MAJOR_TOKENS) if (!m.has(foldSymbol(sym))) m.set(foldSymbol(sym), sym)\n majors = m\n return m\n}\n\n/** The known token a folded symbol reads as: any catalog token on the same chain, or a major one anywhere. */\nfunction lookalikeOf(folded: string, chain: string | undefined): string | undefined {\n if (!folded) return undefined\n if (chain) {\n const same = ccChain(catalogChainId(chain))?.assets.find((a) => foldSymbol(a.symbol) === folded)\n if (same) return same.symbol\n }\n return majorSymbols().get(folded)\n}\n\nconst PLAIN = /^[\\x20-\\x7E]*$/\n/**\n * Letters that pass for Latin ones, by block (the whole block, as for Cyrillic): IPA and spacing\n * modifier letters, Greek, Cyrillic, Armenian, Cherokee, Canadian syllabics, runic, phonetic letters\n * and small capitals (also Latin Extended-D / E), letterlike symbols (ℂ ℍ ℕ — not ™ ℠ ℗), Roman\n * numerals, enclosed letters and digits (Ⓤ ①, 🅤), Coptic, Tifinagh, Lisu, Vai, mathematical letters\n * (𝐔) and the astral Deseret, Osage, Lycian, Carian, Old Italic and Warang Citi; for Georgian, a living\n * script, only its look-alike letters (Ⴝ ყ ჿ Ჽ ⴭ…). Explicit ranges: Hermes has no Unicode property\n * escapes. Emoji, CJK, Arabic, Hebrew, Thai and accented Latin are not here: a caution, not a danger.\n */\nconst LOOKALIKE_LETTERS = /[\\u0250-\\u02FF\\u0370-\\u03FF\\u0400-\\u052F\\u0530-\\u058F\\u10B9\\u10BD\\u10C7\\u10CD\\u10E7\\u10FD\\u10FF\\u13A0-\\u13FF\\u1400-\\u167F\\u16A0-\\u16FF\\u1C80-\\u1C8F\\u1C95\\u1CB7\\u1CBD\\u1CBF\\u1D00-\\u1DBF\\u1F00-\\u1FFF\\u2100-\\u2116\\u2118-\\u211F\\u2121\\u2123-\\u214F\\u2160-\\u2188\\u2460-\\u24FF\\u2C80-\\u2CFF\\u2D2D\\u2D30-\\u2D7F\\u2DE0-\\u2DFF\\uA4D0-\\uA4FF\\uA500-\\uA63F\\uA640-\\uA69F\\uA720-\\uA7FF\\uAB30-\\uABBF]|\\uD835[\\uDC00-\\uDFFF]|\\uD83C[\\uDD10-\\uDD69]|\\uD801[\\uDC00-\\uDC4F\\uDCB0-\\uDCFF]|\\uD800[\\uDE80-\\uDEDF\\uDF00-\\uDF2F]|\\uD806[\\uDCA0-\\uDCFF]/\n/**\n * What hides inside text: {@link INVISIBLE} without the joiner and the variation selectors 1–16\n * emoji are built with.\n */\nconst HIDING = /[\\x00-\\x08\\x0E-\\x1F\\x7F-\\x9F\\u00AD\\u034F\\u061C\\u115F\\u1160\\u17B4\\u17B5\\u180B-\\u180F\\u200B\\u200C\\u200E\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u2800\\u3164\\uFEFF\\uFFA0\\uFFF9-\\uFFFB]|\\uDB40[\\uDC00-\\uDC7F\\uDD00-\\uDDEF]|\\uD834[\\uDD59\\uDD73-\\uDD7A]|\\uD82F[\\uDCA0-\\uDCA3]/\n/** A line break inside a symbol: what follows it is on a line a one-line label does not show. */\nconst LINE_BREAK = /[\\n\\v\\f\\r\\u0085\\u2028\\u2029]/\n/** Emoji built from tag characters (the England, Scotland and Wales flags) are emoji, not hidden text. */\nconst EMOJI_TAG_SEQUENCE = /\\uD83C\\uDFF4(?:\\uDB40[\\uDC20-\\uDC7E])+\\uDB40\\uDC7F/g\n/** Zero-width non-joiner between two non-ASCII characters is spelling (Persian, Indic scripts), not hiding. */\nconst SPELLING_ZWNJ = /([^\\x00-\\x7F])\\u200C(?=[^\\x00-\\x7F])/g\n/** Letters that pass for Latin ones, or characters that hide, in `text`. */\nconst passesOrHides = (text: string): boolean => LOOKALIKE_LETTERS.test(text) || HIDING.test(text.replace(EMOJI_TAG_SEQUENCE, '').replace(SPELLING_ZWNJ, '$1'))\n\n\n/**\n * Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.\n * Pure: nothing is fetched. `impersonation` when its symbol (look-alike letters folded) reads as a\n * catalog token on the same `chain` — the real one would be in the list, this is another contract —\n * or as a native coin or major stablecoin anywhere. A bridged token of another chain's list (JUP on\n * Solana, listed on Unichain) is not flagged.\n */\nexport function assessToken(info: TokenInfo, chain?: string): TokenAssessment {\n const risks: TokenRisk[] = [{ code: 'unlisted', level: 'caution', message: \"Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself.\" }]\n // What the wallet stores and shows (trimmed, cut to TOKEN_SYMBOL_MAX), and the whole text a UI that does\n // not cut it would show: a look-alike in either is one (scan 2026-10-27 modules, F28).\n const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX)\n const name = tokenText(info.name, TOKEN_NAME_MAX)\n const whole = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX).replace(/\\s+/g, '')\n const lookalike = lookalikeOf(foldSymbol(symbol), chain) ?? lookalikeOf(foldSymbol(whole), chain)\n if (lookalike) {\n risks.push({ code: 'impersonation', level: 'danger', message: `It calls itself ${JSON.stringify(symbol)}, like ${lookalike}, but it is a different token at a different address.` })\n }\n // The raw head (what a host showing the raw symbol shows) and the stored symbol (what this wallet shows):\n // leading spaces must not push the look-alike out of the part that is checked.\n const head = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX)\n if (passesOrHides(head) || passesOrHides(symbol) || LINE_BREAK.test(symbol)) {\n // Letters of another alphabet that pass for Latin ones, or characters that hide, are how a token passes for a\n // known one (scan 2026-10-27 modules, F36). Emoji, CJK and accented Latin stay a caution (owner decision 2026-10-28).\n risks.push({ code: 'odd-characters', level: 'danger', message: 'Its symbol uses letters of another alphabet that pass for Latin ones (Cyrillic, Greek…) or characters that hide in text.' })\n } else if (!PLAIN.test(symbol) || /\\s/.test(symbol) || !PLAIN.test(name) || name !== name.replace(INVISIBLE, '')) {\n risks.push({ code: 'odd-characters', level: 'caution', message: 'Its symbol or name uses unusual characters (emoji, other scripts, accents), spaces inside the symbol, or invisible ones.' })\n }\n if (!symbol) risks.push({ code: 'no-symbol', level: 'caution', message: 'The token reports no symbol.' })\n if (info.totalSupply === 0n) risks.push({ code: 'zero-supply', level: 'caution', message: 'Nothing has been minted.' })\n if (info.decimals > 24) risks.push({ code: 'unusual-decimals', level: 'caution', message: `It uses ${info.decimals} decimals; amounts may display oddly.` })\n for (const flag of info.flags) {\n const r = FLAG_RISKS[flag]\n if (r) risks.push({ code: flag, ...r })\n }\n risks.sort((a, b) => (a.level === b.level ? 0 : a.level === 'danger' ? -1 : 1))\n return { level: risks.some((r) => r.level === 'danger') ? 'danger' : 'caution', risks }\n}\n","import type { IWalletAccountReadOnly, TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { catalogChainId, ccAssetByToken, isTokenAddress, type CcAsset } from '@anywalletkit/swap2-assets'\nimport { TOKEN_NAME_MAX, TOKEN_SYMBOL_MAX, assessToken, tokenText, type TokenAssessment } from './assess.js'\n\n/** One JSON value under one key — the shape every small store needs (AsyncStorage, chrome.storage, memory). */\nexport interface JsonStorage<T> {\n /** The stored value, or `null` on first run. */\n get(): Promise<T | null>\n /** Persist the value. */\n set(value: T): Promise<void>\n}\n\n/** In-memory storage for tests and smoke runs. */\nexport function memoryJsonStorage<T>(initial: T | null = null): JsonStorage<T> & { value: T | null } {\n const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T\n const s = {\n value: initial,\n async get() { return s.value === null ? null : clone(s.value) },\n async set(value: T) { s.value = clone(value) },\n }\n return s\n}\n\n/** A token the user added. `symbol` and `name` are what the token reports: display them, never match on them. */\nexport interface UserToken {\n /** CAIP-2 chain id, the catalog's key (`solana:mainnet`, `eip155:1`). */\n chain: string\n /** Contract (EVM, checksummed as read) or mint (Solana). The token's identity. */\n address: string\n symbol: string\n name: string\n decimals: number\n /** `erc20`, `spl` or `spl-token-2022`. */\n standard: string\n /** Facts read from the chain when it was added. */\n flags: TokenFlag[]\n /** When the user added it (ms since epoch). */\n addedAt: number\n}\n\n/** What {@link TokensTable} stores. */\nexport interface TokensState {\n v: 1\n tokens: UserToken[]\n}\n\n/** Codes of {@link TokensError}. */\nexport type TokensErrorCode = 'not-loaded' | 'unsupported-chain' | 'bad-address' | 'listed' | 'bad-token' | 'full'\n\n/** A refusal from the tokens table or {@link inspectToken}, with a machine-readable `code`. */\nexport class TokensError extends Error {\n readonly code: TokensErrorCode\n constructor(code: TokensErrorCode, message: string) {\n super(message)\n this.name = 'TokensError'\n this.code = code\n }\n}\n\nconst sameAddress = (chain: string, a: string, b: string) => (chain.startsWith('eip155:') ? a.toLowerCase() === b.toLowerCase() : a === b)\nconst FLAGS: ReadonlySet<string> = new Set<TokenFlag>(['upgradeable', 'mint-authority', 'freeze-authority', 'permanent-delegate', 'transfer-fee', 'transfer-hook', 'non-transferable', 'default-frozen', 'pausable'])\n\n/** A stored entry as it must be, or null (a hand-edited or corrupted entry is dropped, never trusted). */\nfunction validEntry(t: unknown): UserToken | null {\n if (!t || typeof t !== 'object') return null\n const e = t as Record<string, unknown>\n if (typeof e.chain !== 'string' || typeof e.address !== 'string' || !isTokenAddress(e.chain, e.address)) return null\n if (typeof e.decimals !== 'number' || !Number.isInteger(e.decimals) || e.decimals < 0 || e.decimals > 77) return null\n if (typeof e.symbol !== 'string' || typeof e.name !== 'string' || typeof e.standard !== 'string' || typeof e.addedAt !== 'number') return null\n if (!Array.isArray(e.flags) || !e.flags.every((f) => typeof f === 'string' && FLAGS.has(f))) return null\n return {\n chain: e.chain, address: e.address, symbol: tokenText(e.symbol, TOKEN_SYMBOL_MAX), name: tokenText(e.name, TOKEN_NAME_MAX), decimals: e.decimals,\n standard: e.standard, flags: [...e.flags] as TokenFlag[], addedAt: e.addedAt,\n }\n}\n\n/** The result of looking a token up before adding it. */\nexport type TokenInspection =\n | { listed: CcAsset }\n | { listed?: undefined; info: TokenInfo; assessment: TokenAssessment; token: UserToken }\n\n/**\n * Read a token from the chain through `account` (its chain decides where) and assess it. A token the\n * catalog already lists comes back as `{ listed }`: nothing to add. Otherwise the token, ready for\n * {@link TokensTable.add}, and the warnings to show first. Throws {@link TokensError}\n * (`unsupported-chain`, `bad-address`) or the chain package's `NotATokenError`.\n */\nexport async function inspectToken(\n account: Pick<IWalletAccountReadOnly, 'chain' | 'getTokenInfo'> & { getChainId?: () => Promise<number | bigint> },\n address: string,\n now: () => number = Date.now,\n): Promise<TokenInspection> {\n let reported = account.chain\n // An EVM account created without a chain id reports 'eip155:unknown' until it has asked its RPC:\n // ask now, or the catalog check (already listed? a look-alike of this chain's tokens?) would miss.\n if (reported === 'eip155:unknown') {\n if (typeof account.getChainId !== 'function') throw new TokensError('unsupported-chain', 'The account does not know its chain id; create it with chainId.')\n reported = `eip155:${String(await account.getChainId())}`\n }\n const chain = catalogChainId(reported)\n if (!chain.startsWith('eip155:') && !chain.startsWith('solana:')) throw new TokensError('unsupported-chain', `Adding tokens is supported on EVM chains and Solana, not ${account.chain}.`)\n const ref = address.trim()\n if (!isTokenAddress(chain, ref)) throw new TokensError('bad-address', `${JSON.stringify(address)} is not a token address on ${chain}.`)\n const listed = ccAssetByToken(chain, ref)\n if (listed) return { listed }\n if (typeof account.getTokenInfo !== 'function') throw new TokensError('unsupported-chain', `This ${account.chain} account cannot read token info.`)\n const info = await account.getTokenInfo(ref)\n // What is stored is what is assessed: trimmed and cut first (scan 2026-10-27 modules, F28).\n const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX)\n const name = tokenText(info.name, TOKEN_NAME_MAX)\n const token: UserToken = {\n chain, address: info.address, symbol, name, decimals: info.decimals,\n standard: info.standard, flags: [...info.flags], addedAt: now(),\n }\n return { info, assessment: assessToken({ ...info, symbol, name }, chain), token }\n}\n\n/**\n * The swap options that name a user token as the source: its address and decimals. Pass them to an\n * engine created with `allowUnlistedTokens: true`.\n */\nexport function swapSource(token: Pick<UserToken, 'address' | 'decimals'>): { fromToken: string; fromTokenDecimals: number } {\n return { fromToken: token.address, fromTokenDecimals: token.decimals }\n}\n\n/**\n * The user's own tokens, over any JSON storage: load once at cold start, then read synchronously.\n * Writes queue behind each other; a failed save leaves memory as stored. Tokens the catalog lists are\n * refused (they are already there), and so are addresses that are not EVM contracts or Solana mints.\n */\nexport class TokensTable {\n private _state: TokensState | null = null\n private _loading: Promise<TokensState> | null = null\n private _writes: Promise<unknown> = Promise.resolve()\n private readonly listeners = new Set<() => void>()\n readonly maxTokens: number\n\n constructor(private readonly storage: JsonStorage<TokensState>, options: { maxTokens?: number } = {}) {\n this.maxTokens = options.maxTokens ?? 200\n }\n\n /** Read the stored list. Call once before the sync reads; a failed read throws and leaves the table unloaded. */\n async load(): Promise<void> {\n await this.state()\n }\n\n /** Called after every successful write. Returns the unsubscribe function. */\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => { this.listeners.delete(fn) }\n }\n\n /** The user's tokens, oldest first; only those on `chain` when given. */\n list(chain?: string): UserToken[] {\n const s = this.loaded()\n const want = chain === undefined ? undefined : catalogChainId(chain)\n return s.tokens.filter((t) => want === undefined || t.chain === want).map((t) => ({ ...t, flags: [...t.flags] }))\n }\n\n /** The user token at `address` on `chain`, if added. */\n get(chain: string, address: string): UserToken | undefined {\n const c = catalogChainId(chain)\n const t = this.loaded().tokens.find((x) => x.chain === c && sameAddress(c, x.address, address))\n return t ? { ...t, flags: [...t.flags] } : undefined\n }\n\n /** Add (or refresh) a token — usually `inspection.token` after the user accepted the warnings. */\n async add(token: UserToken): Promise<UserToken> {\n const entry = validEntry({ ...token, chain: catalogChainId(token.chain) })\n if (!entry) throw new TokensError('bad-token', 'Not a valid token entry (address, decimals 0–77, symbol, name, standard, flags).')\n if (ccAssetByToken(entry.chain, entry.address)) throw new TokensError('listed', `${entry.address} is already in the wallet's token list.`)\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === entry.chain && sameAddress(entry.chain, t.address, entry.address)))\n if (rest.length >= this.maxTokens) throw new TokensError('full', `At most ${this.maxTokens} tokens can be added.`)\n return { v: 1, tokens: [...rest, entry] }\n })\n return { ...entry, flags: [...entry.flags] }\n }\n\n /** Remove a token. Returns whether it was there. */\n async remove(chain: string, address: string): Promise<boolean> {\n const c = catalogChainId(chain)\n let removed = false\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === c && sameAddress(c, t.address, address)))\n removed = rest.length !== s.tokens.length\n return { v: 1, tokens: rest }\n })\n return removed\n }\n\n private loaded(): TokensState {\n if (!this._state) throw new TokensError('not-loaded', 'Call load() before reading the tokens table.')\n return this._state\n }\n\n private async state(): Promise<TokensState> {\n if (this._state) return this._state\n this._loading ??= (async () => {\n let stored: TokensState | null\n try {\n stored = await this.storage.get()\n } catch (e) {\n // A failed read is not an empty list: the next write would erase the user's tokens.\n this._loading = null\n throw e\n }\n const tokens = stored && stored.v === 1 && Array.isArray(stored.tokens)\n ? stored.tokens.map(validEntry).filter((t): t is UserToken => t !== null)\n : []\n this._state = { v: 1, tokens }\n return this._state\n })()\n return this._loading\n }\n\n private update(change: (s: TokensState) => TokensState): Promise<void> {\n const run = this._writes.then(async () => {\n const next = change(await this.state())\n await this.storage.set(next)\n this._state = next\n for (const fn of this.listeners) {\n try { fn() } catch { /* a listener's failure is its own */ }\n }\n })\n this._writes = run.catch(() => undefined)\n return run\n }\n}\n"],"mappings":"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","names":["import_swap2_assets"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -25,7 +25,21 @@ interface TokenAssessment {
|
|
|
25
25
|
level: TokenRiskLevel;
|
|
26
26
|
risks: TokenRisk[];
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/** Longest symbol the wallet stores and assesses (code points); a longer one is cut, after trimming, before anything reads it. */
|
|
29
|
+
declare const TOKEN_SYMBOL_MAX = 32;
|
|
30
|
+
/** Longest name the wallet stores (code points). */
|
|
31
|
+
declare const TOKEN_NAME_MAX = 128;
|
|
32
|
+
/**
|
|
33
|
+
* A symbol or name as the wallet keeps and shows it: trimmed, cut to `max` code points, trimmed again —
|
|
34
|
+
* so "USDC" + 124 spaces + "X" is stored, shown and assessed as "USDC" (scan 2026-10-27 modules, F28).
|
|
35
|
+
*/
|
|
36
|
+
declare function tokenText(text: string, max: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* A symbol folded the way a reader sees it — a confusables skeleton in the spirit of UTS #39: invisible
|
|
39
|
+
* characters and spaces gone, compatibility forms unified and accents dropped (NFKD), look-alike letters
|
|
40
|
+
* mapped (Cyrillic, Greek, currency signs, and ASCII l / 1 / | → I, 0 → O; rn → m, cl → d, vv → w), upper
|
|
41
|
+
* case. Only the first {@link TOKEN_SYMBOL_MAX} code points are read (scan 2026-10-27 modules, F36).
|
|
42
|
+
*/
|
|
29
43
|
declare function foldSymbol(text: string): string;
|
|
30
44
|
/**
|
|
31
45
|
* Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.
|
|
@@ -133,4 +147,4 @@ declare class TokensTable {
|
|
|
133
147
|
private update;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
export { type JsonStorage, type TokenAssessment, type TokenInspection, type TokenRisk, type TokenRiskCode, type TokenRiskLevel, TokensError, type TokensErrorCode, type TokensState, TokensTable, type UserToken, assessToken, foldSymbol, inspectToken, memoryJsonStorage, swapSource };
|
|
150
|
+
export { type JsonStorage, TOKEN_NAME_MAX, TOKEN_SYMBOL_MAX, type TokenAssessment, type TokenInspection, type TokenRisk, type TokenRiskCode, type TokenRiskLevel, TokensError, type TokensErrorCode, type TokensState, TokensTable, type UserToken, assessToken, foldSymbol, inspectToken, memoryJsonStorage, swapSource, tokenText };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,21 @@ interface TokenAssessment {
|
|
|
25
25
|
level: TokenRiskLevel;
|
|
26
26
|
risks: TokenRisk[];
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/** Longest symbol the wallet stores and assesses (code points); a longer one is cut, after trimming, before anything reads it. */
|
|
29
|
+
declare const TOKEN_SYMBOL_MAX = 32;
|
|
30
|
+
/** Longest name the wallet stores (code points). */
|
|
31
|
+
declare const TOKEN_NAME_MAX = 128;
|
|
32
|
+
/**
|
|
33
|
+
* A symbol or name as the wallet keeps and shows it: trimmed, cut to `max` code points, trimmed again —
|
|
34
|
+
* so "USDC" + 124 spaces + "X" is stored, shown and assessed as "USDC" (scan 2026-10-27 modules, F28).
|
|
35
|
+
*/
|
|
36
|
+
declare function tokenText(text: string, max: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* A symbol folded the way a reader sees it — a confusables skeleton in the spirit of UTS #39: invisible
|
|
39
|
+
* characters and spaces gone, compatibility forms unified and accents dropped (NFKD), look-alike letters
|
|
40
|
+
* mapped (Cyrillic, Greek, currency signs, and ASCII l / 1 / | → I, 0 → O; rn → m, cl → d, vv → w), upper
|
|
41
|
+
* case. Only the first {@link TOKEN_SYMBOL_MAX} code points are read (scan 2026-10-27 modules, F36).
|
|
42
|
+
*/
|
|
29
43
|
declare function foldSymbol(text: string): string;
|
|
30
44
|
/**
|
|
31
45
|
* Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.
|
|
@@ -133,4 +147,4 @@ declare class TokensTable {
|
|
|
133
147
|
private update;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
export { type JsonStorage, type TokenAssessment, type TokenInspection, type TokenRisk, type TokenRiskCode, type TokenRiskLevel, TokensError, type TokensErrorCode, type TokensState, TokensTable, type UserToken, assessToken, foldSymbol, inspectToken, memoryJsonStorage, swapSource };
|
|
150
|
+
export { type JsonStorage, TOKEN_NAME_MAX, TOKEN_SYMBOL_MAX, type TokenAssessment, type TokenInspection, type TokenRisk, type TokenRiskCode, type TokenRiskLevel, TokensError, type TokensErrorCode, type TokensState, TokensTable, type UserToken, assessToken, foldSymbol, inspectToken, memoryJsonStorage, swapSource, tokenText };
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,17 @@ var FLAG_RISKS = {
|
|
|
11
11
|
"default-frozen": { level: "caution", message: "New holders start frozen until the issuer lets them move it." },
|
|
12
12
|
"pausable": { level: "caution", message: "The issuer can pause every transfer." }
|
|
13
13
|
};
|
|
14
|
+
var TOKEN_SYMBOL_MAX = 32;
|
|
15
|
+
var TOKEN_NAME_MAX = 128;
|
|
16
|
+
function tokenText(text, max) {
|
|
17
|
+
let out = "";
|
|
18
|
+
let n = 0;
|
|
19
|
+
for (const ch of text.trim()) {
|
|
20
|
+
if (n++ >= max) break;
|
|
21
|
+
out += ch;
|
|
22
|
+
}
|
|
23
|
+
return out.trim();
|
|
24
|
+
}
|
|
14
25
|
var CONFUSABLES = {
|
|
15
26
|
"\u0410": "A",
|
|
16
27
|
"\u0412": "B",
|
|
@@ -73,13 +84,94 @@ var CONFUSABLES = {
|
|
|
73
84
|
"\u20AC": "E",
|
|
74
85
|
"\xA2": "C",
|
|
75
86
|
"\u20BF": "B",
|
|
76
|
-
"\u039E": "E"
|
|
87
|
+
"\u039E": "E",
|
|
88
|
+
// ASCII and Latin look-alikes of I and O (UTS #39 confusables): "DAl" reads as DAI, "USDl" as USD1.
|
|
89
|
+
"l": "I",
|
|
90
|
+
"1": "I",
|
|
91
|
+
"|": "I",
|
|
92
|
+
"\u0131": "I",
|
|
93
|
+
"\u0142": "I",
|
|
94
|
+
"\u01C0": "I",
|
|
95
|
+
"0": "O",
|
|
96
|
+
"\xF8": "O",
|
|
97
|
+
"\xD8": "O",
|
|
98
|
+
// Latin letters without a decomposition, and Latin-1 accented letters for runtimes without String.prototype.normalize.
|
|
99
|
+
"\xD0": "D",
|
|
100
|
+
"\xF0": "D",
|
|
101
|
+
"\u0110": "D",
|
|
102
|
+
"\u0111": "D",
|
|
103
|
+
"\u0126": "H",
|
|
104
|
+
"\u0127": "H",
|
|
105
|
+
"\u0166": "T",
|
|
106
|
+
"\u0167": "T",
|
|
107
|
+
"\xDF": "B",
|
|
108
|
+
"\xDE": "P",
|
|
109
|
+
"\xFE": "P",
|
|
110
|
+
"\u014A": "N",
|
|
111
|
+
"\u014B": "N",
|
|
112
|
+
"\xC0": "A",
|
|
113
|
+
"\xC1": "A",
|
|
114
|
+
"\xC2": "A",
|
|
115
|
+
"\xC3": "A",
|
|
116
|
+
"\xC4": "A",
|
|
117
|
+
"\xC5": "A",
|
|
118
|
+
"\xE0": "A",
|
|
119
|
+
"\xE1": "A",
|
|
120
|
+
"\xE2": "A",
|
|
121
|
+
"\xE3": "A",
|
|
122
|
+
"\xE4": "A",
|
|
123
|
+
"\xE5": "A",
|
|
124
|
+
"\xC7": "C",
|
|
125
|
+
"\xE7": "C",
|
|
126
|
+
"\xC8": "E",
|
|
127
|
+
"\xC9": "E",
|
|
128
|
+
"\xCA": "E",
|
|
129
|
+
"\xCB": "E",
|
|
130
|
+
"\xE8": "E",
|
|
131
|
+
"\xE9": "E",
|
|
132
|
+
"\xEA": "E",
|
|
133
|
+
"\xEB": "E",
|
|
134
|
+
"\xCC": "I",
|
|
135
|
+
"\xCD": "I",
|
|
136
|
+
"\xCE": "I",
|
|
137
|
+
"\xCF": "I",
|
|
138
|
+
"\xEC": "I",
|
|
139
|
+
"\xED": "I",
|
|
140
|
+
"\xEE": "I",
|
|
141
|
+
"\xEF": "I",
|
|
142
|
+
"\xD1": "N",
|
|
143
|
+
"\xF1": "N",
|
|
144
|
+
"\xD2": "O",
|
|
145
|
+
"\xD3": "O",
|
|
146
|
+
"\xD4": "O",
|
|
147
|
+
"\xD5": "O",
|
|
148
|
+
"\xD6": "O",
|
|
149
|
+
"\xF2": "O",
|
|
150
|
+
"\xF3": "O",
|
|
151
|
+
"\xF4": "O",
|
|
152
|
+
"\xF5": "O",
|
|
153
|
+
"\xF6": "O",
|
|
154
|
+
"\xD9": "U",
|
|
155
|
+
"\xDA": "U",
|
|
156
|
+
"\xDB": "U",
|
|
157
|
+
"\xDC": "U",
|
|
158
|
+
"\xF9": "U",
|
|
159
|
+
"\xFA": "U",
|
|
160
|
+
"\xFB": "U",
|
|
161
|
+
"\xFC": "U",
|
|
162
|
+
"\xDD": "Y",
|
|
163
|
+
"\xFD": "Y",
|
|
164
|
+
"\xFF": "Y"
|
|
77
165
|
};
|
|
78
|
-
var
|
|
166
|
+
var PAIRS = [[/rn/g, "m"], [/cl/g, "d"], [/vv|VV/g, "W"]];
|
|
167
|
+
var MARKS = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/g;
|
|
168
|
+
var INVISIBLE = /[\x00-\x08\x0E-\x1F\x7F-\x9F\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2800\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF9-\uFFFB]|\uDB40[\uDC00-\uDC7F\uDD00-\uDDEF]|\uD834[\uDD59\uDD73-\uDD7A]|\uD82F[\uDCA0-\uDCA3]/g;
|
|
79
169
|
function foldSymbol(text) {
|
|
80
|
-
let s = text
|
|
81
|
-
if (typeof s.normalize === "function") s = s.normalize("
|
|
170
|
+
let s = tokenText(text, TOKEN_SYMBOL_MAX).replace(INVISIBLE, "");
|
|
171
|
+
if (typeof s.normalize === "function") s = s.normalize("NFKD").replace(MARKS, "");
|
|
172
|
+
s = s.replace(/\s+/g, "");
|
|
82
173
|
s = s.replace(/[\uFF01-\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 65248));
|
|
174
|
+
for (const [re, to] of PAIRS) s = s.replace(re, to);
|
|
83
175
|
let out = "";
|
|
84
176
|
for (const ch of s) out += CONFUSABLES[ch] ?? ch;
|
|
85
177
|
return out.toUpperCase();
|
|
@@ -103,17 +195,28 @@ function lookalikeOf(folded, chain) {
|
|
|
103
195
|
return majorSymbols().get(folded);
|
|
104
196
|
}
|
|
105
197
|
var PLAIN = /^[\x20-\x7E]*$/;
|
|
198
|
+
var LOOKALIKE_LETTERS = /[\u0250-\u02FF\u0370-\u03FF\u0400-\u052F\u0530-\u058F\u10B9\u10BD\u10C7\u10CD\u10E7\u10FD\u10FF\u13A0-\u13FF\u1400-\u167F\u16A0-\u16FF\u1C80-\u1C8F\u1C95\u1CB7\u1CBD\u1CBF\u1D00-\u1DBF\u1F00-\u1FFF\u2100-\u2116\u2118-\u211F\u2121\u2123-\u214F\u2160-\u2188\u2460-\u24FF\u2C80-\u2CFF\u2D2D\u2D30-\u2D7F\u2DE0-\u2DFF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA720-\uA7FF\uAB30-\uABBF]|\uD835[\uDC00-\uDFFF]|\uD83C[\uDD10-\uDD69]|\uD801[\uDC00-\uDC4F\uDCB0-\uDCFF]|\uD800[\uDE80-\uDEDF\uDF00-\uDF2F]|\uD806[\uDCA0-\uDCFF]/;
|
|
199
|
+
var HIDING = /[\x00-\x08\x0E-\x1F\x7F-\x9F\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B\u200C\u200E\u200F\u202A-\u202E\u2060-\u206F\u2800\u3164\uFEFF\uFFA0\uFFF9-\uFFFB]|\uDB40[\uDC00-\uDC7F\uDD00-\uDDEF]|\uD834[\uDD59\uDD73-\uDD7A]|\uD82F[\uDCA0-\uDCA3]/;
|
|
200
|
+
var LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
201
|
+
var EMOJI_TAG_SEQUENCE = /\uD83C\uDFF4(?:\uDB40[\uDC20-\uDC7E])+\uDB40\uDC7F/g;
|
|
202
|
+
var SPELLING_ZWNJ = /([^\x00-\x7F])\u200C(?=[^\x00-\x7F])/g;
|
|
203
|
+
var passesOrHides = (text) => LOOKALIKE_LETTERS.test(text) || HIDING.test(text.replace(EMOJI_TAG_SEQUENCE, "").replace(SPELLING_ZWNJ, "$1"));
|
|
106
204
|
function assessToken(info, chain) {
|
|
107
205
|
const risks = [{ code: "unlisted", level: "caution", message: "Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself." }];
|
|
108
|
-
const
|
|
109
|
-
const
|
|
206
|
+
const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX);
|
|
207
|
+
const name = tokenText(info.name, TOKEN_NAME_MAX);
|
|
208
|
+
const whole = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX).replace(/\s+/g, "");
|
|
209
|
+
const lookalike = lookalikeOf(foldSymbol(symbol), chain) ?? lookalikeOf(foldSymbol(whole), chain);
|
|
110
210
|
if (lookalike) {
|
|
111
|
-
risks.push({ code: "impersonation", level: "danger", message: `It calls itself ${JSON.stringify(
|
|
211
|
+
risks.push({ code: "impersonation", level: "danger", message: `It calls itself ${JSON.stringify(symbol)}, like ${lookalike}, but it is a different token at a different address.` });
|
|
112
212
|
}
|
|
113
|
-
|
|
114
|
-
|
|
213
|
+
const head = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX);
|
|
214
|
+
if (passesOrHides(head) || passesOrHides(symbol) || LINE_BREAK.test(symbol)) {
|
|
215
|
+
risks.push({ code: "odd-characters", level: "danger", message: "Its symbol uses letters of another alphabet that pass for Latin ones (Cyrillic, Greek\u2026) or characters that hide in text." });
|
|
216
|
+
} else if (!PLAIN.test(symbol) || /\s/.test(symbol) || !PLAIN.test(name) || name !== name.replace(INVISIBLE, "")) {
|
|
217
|
+
risks.push({ code: "odd-characters", level: "caution", message: "Its symbol or name uses unusual characters (emoji, other scripts, accents), spaces inside the symbol, or invisible ones." });
|
|
115
218
|
}
|
|
116
|
-
if (!
|
|
219
|
+
if (!symbol) risks.push({ code: "no-symbol", level: "caution", message: "The token reports no symbol." });
|
|
117
220
|
if (info.totalSupply === 0n) risks.push({ code: "zero-supply", level: "caution", message: "Nothing has been minted." });
|
|
118
221
|
if (info.decimals > 24) risks.push({ code: "unusual-decimals", level: "caution", message: `It uses ${info.decimals} decimals; amounts may display oddly.` });
|
|
119
222
|
for (const flag of info.flags) {
|
|
@@ -159,8 +262,8 @@ function validEntry(t) {
|
|
|
159
262
|
return {
|
|
160
263
|
chain: e.chain,
|
|
161
264
|
address: e.address,
|
|
162
|
-
symbol: e.symbol
|
|
163
|
-
name: e.name
|
|
265
|
+
symbol: tokenText(e.symbol, TOKEN_SYMBOL_MAX),
|
|
266
|
+
name: tokenText(e.name, TOKEN_NAME_MAX),
|
|
164
267
|
decimals: e.decimals,
|
|
165
268
|
standard: e.standard,
|
|
166
269
|
flags: [...e.flags],
|
|
@@ -181,17 +284,19 @@ async function inspectToken(account, address, now = Date.now) {
|
|
|
181
284
|
if (listed) return { listed };
|
|
182
285
|
if (typeof account.getTokenInfo !== "function") throw new TokensError("unsupported-chain", `This ${account.chain} account cannot read token info.`);
|
|
183
286
|
const info = await account.getTokenInfo(ref);
|
|
287
|
+
const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX);
|
|
288
|
+
const name = tokenText(info.name, TOKEN_NAME_MAX);
|
|
184
289
|
const token = {
|
|
185
290
|
chain,
|
|
186
291
|
address: info.address,
|
|
187
|
-
symbol
|
|
188
|
-
name
|
|
292
|
+
symbol,
|
|
293
|
+
name,
|
|
189
294
|
decimals: info.decimals,
|
|
190
295
|
standard: info.standard,
|
|
191
296
|
flags: [...info.flags],
|
|
192
297
|
addedAt: now()
|
|
193
298
|
};
|
|
194
|
-
return { info, assessment: assessToken(info, chain), token };
|
|
299
|
+
return { info, assessment: assessToken({ ...info, symbol, name }, chain), token };
|
|
195
300
|
}
|
|
196
301
|
function swapSource(token) {
|
|
197
302
|
return { fromToken: token.address, fromTokenDecimals: token.decimals };
|
|
@@ -290,12 +395,15 @@ var TokensTable = class {
|
|
|
290
395
|
}
|
|
291
396
|
};
|
|
292
397
|
export {
|
|
398
|
+
TOKEN_NAME_MAX,
|
|
399
|
+
TOKEN_SYMBOL_MAX,
|
|
293
400
|
TokensError,
|
|
294
401
|
TokensTable,
|
|
295
402
|
assessToken,
|
|
296
403
|
foldSymbol,
|
|
297
404
|
inspectToken,
|
|
298
405
|
memoryJsonStorage,
|
|
299
|
-
swapSource
|
|
406
|
+
swapSource,
|
|
407
|
+
tokenText
|
|
300
408
|
};
|
|
301
409
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assess.ts","../src/table.ts"],"sourcesContent":["import type { TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { CC_SOURCE_CHAINS, catalogChainId, ccChain } from '@anywalletkit/swap2-assets'\n\n/** `danger`: the token can take or trap the user's money, or pretends to be another one. `caution`: worth knowing. */\nexport type TokenRiskLevel = 'danger' | 'caution'\n\n/**\n * Why a token deserves a warning:\n * - `unlisted`: always — the wallet's list does not include it, and anyone can deploy a token with any name and logo.\n * - `impersonation`: its symbol reads as a known token's (USDC, ETH, …), look-alike letters included.\n * - `odd-characters`: its symbol or name uses characters outside plain ASCII, or invisible ones.\n * - `no-symbol`: it reports no symbol.\n * - `zero-supply`: nothing has been minted.\n * - `unusual-decimals`: more than 24 decimals.\n * - a {@link TokenFlag} read from the chain (issuer powers, Token-2022 extensions).\n */\nexport type TokenRiskCode = 'unlisted' | 'impersonation' | 'odd-characters' | 'no-symbol' | 'zero-supply' | 'unusual-decimals' | TokenFlag\n\n/** One warning, worded for the person adding the token. */\nexport interface TokenRisk {\n code: TokenRiskCode\n level: TokenRiskLevel\n message: string\n}\n\n/** Every warning that applies, worst first, and the worst level. A user token is never \"safe\": `caution` is the floor. */\nexport interface TokenAssessment {\n level: TokenRiskLevel\n risks: TokenRisk[]\n}\n\nconst FLAG_RISKS: Readonly<Record<TokenFlag, { level: TokenRiskLevel; message: string }>> = {\n 'upgradeable': { level: 'caution', message: \"The token's owner can replace its code.\" },\n 'mint-authority': { level: 'caution', message: 'The issuer can mint more at any time.' },\n 'freeze-authority': { level: 'caution', message: 'The issuer can freeze your balance.' },\n 'permanent-delegate': { level: 'danger', message: 'The issuer can move or burn the tokens in your wallet.' },\n 'transfer-fee': { level: 'caution', message: 'Every transfer pays a fee to the issuer.' },\n 'transfer-hook': { level: 'danger', message: \"A program of the issuer's choosing runs on every transfer: it can block selling.\" },\n 'non-transferable': { level: 'danger', message: 'It cannot be moved: not sent, not swapped.' },\n 'default-frozen': { level: 'caution', message: 'New holders start frozen until the issuer lets them move it.' },\n 'pausable': { level: 'caution', message: 'The issuer can pause every transfer.' },\n}\n\n/** Letters that render like Latin capitals (Cyrillic, Greek) and currency signs used as letters. */\nconst CONFUSABLES: Readonly<Record<string, string>> = {\n 'А': 'A', 'В': 'B', 'Е': 'E', 'К': 'K', 'М': 'M', 'Н': 'H', 'О': 'O', 'Р': 'P', 'С': 'C', 'Т': 'T', 'Х': 'X', 'У': 'Y', 'І': 'I', 'Ј': 'J', 'Ѕ': 'S', 'Ԁ': 'D',\n 'а': 'A', 'в': 'B', 'е': 'E', 'к': 'K', 'м': 'M', 'н': 'H', 'о': 'O', 'р': 'P', 'с': 'C', 'т': 'T', 'х': 'X', 'у': 'Y', 'і': 'I', 'ј': 'J', 'ѕ': 'S', 'ԁ': 'D',\n 'Α': 'A', 'Β': 'B', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Ι': 'I', 'Κ': 'K', 'Μ': 'M', 'Ν': 'N', 'Ο': 'O', 'Ρ': 'P', 'Τ': 'T', 'Υ': 'Y', 'Χ': 'X',\n 'α': 'A', 'ε': 'E', 'ι': 'I', 'κ': 'K', 'ν': 'V', 'ο': 'O', 'ρ': 'P', 'τ': 'T', 'υ': 'U', 'χ': 'X',\n '$': 'S', '₮': 'T', '€': 'E', '¢': 'C', '₿': 'B', 'Ξ': 'E',\n}\n/** Zero-width, bidi and other invisible format characters. */\nconst INVISIBLE = /[͏ᅟᅠ឴឵᠋-᠏---ㅤ︀-️ᅠ]/g\n\n/** A symbol folded the way a reader sees it: invisible characters gone, compatibility forms unified, look-alikes mapped, upper case. */\nexport function foldSymbol(text: string): string {\n let s = text.replace(INVISIBLE, '').replace(/\\s+/g, '')\n if (typeof s.normalize === 'function') s = s.normalize('NFKC')\n // Fullwidth ASCII by hand too: some Hermes builds ship without String.prototype.normalize.\n s = s.replace(/[\\uFF01-\\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))\n let out = ''\n for (const ch of s) out += CONFUSABLES[ch] ?? ch\n return out.toUpperCase()\n}\n\n/** Symbols nobody should reuse on any chain: the big stablecoins and wrapped coins (natives are added from the catalog). */\nconst MAJOR_TOKENS = ['USDT', 'USDC', 'DAI', 'USDe', 'USDS', 'PYUSD', 'FDUSD', 'USD1', 'USDG', 'EURC', 'WETH', 'WBTC', 'cbBTC', 'stETH', 'wstETH', 'WSOL']\n\nlet majors: ReadonlyMap<string, string> | undefined\n/** Folded → display: every chain's native coin from the catalog, plus {@link MAJOR_TOKENS}. */\nfunction majorSymbols(): ReadonlyMap<string, string> {\n if (majors) return majors\n const m = new Map<string, string>()\n for (const chain of CC_SOURCE_CHAINS) for (const a of chain.assets) if (!a.contract && !m.has(foldSymbol(a.symbol))) m.set(foldSymbol(a.symbol), a.symbol)\n for (const sym of MAJOR_TOKENS) if (!m.has(foldSymbol(sym))) m.set(foldSymbol(sym), sym)\n majors = m\n return m\n}\n\n/** The known token a folded symbol reads as: any catalog token on the same chain, or a major one anywhere. */\nfunction lookalikeOf(folded: string, chain: string | undefined): string | undefined {\n if (!folded) return undefined\n if (chain) {\n const same = ccChain(catalogChainId(chain))?.assets.find((a) => foldSymbol(a.symbol) === folded)\n if (same) return same.symbol\n }\n return majorSymbols().get(folded)\n}\n\nconst PLAIN = /^[\\x20-\\x7E]*$/\n\n/**\n * Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.\n * Pure: nothing is fetched. `impersonation` when its symbol (look-alike letters folded) reads as a\n * catalog token on the same `chain` — the real one would be in the list, this is another contract —\n * or as a native coin or major stablecoin anywhere. A bridged token of another chain's list (JUP on\n * Solana, listed on Unichain) is not flagged.\n */\nexport function assessToken(info: TokenInfo, chain?: string): TokenAssessment {\n const risks: TokenRisk[] = [{ code: 'unlisted', level: 'caution', message: \"Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself.\" }]\n const folded = foldSymbol(info.symbol)\n const lookalike = lookalikeOf(folded, chain)\n if (lookalike) {\n risks.push({ code: 'impersonation', level: 'danger', message: `It calls itself ${JSON.stringify(info.symbol)}, like ${lookalike}, but it is a different token at a different address.` })\n }\n if (!PLAIN.test(info.symbol) || !PLAIN.test(info.name) || info.symbol !== info.symbol.replace(INVISIBLE, '') || info.name !== info.name.replace(INVISIBLE, '')) {\n risks.push({ code: 'odd-characters', level: 'caution', message: 'Its symbol or name uses characters that can look like other letters, or invisible ones.' })\n }\n if (!info.symbol.trim()) risks.push({ code: 'no-symbol', level: 'caution', message: 'The token reports no symbol.' })\n if (info.totalSupply === 0n) risks.push({ code: 'zero-supply', level: 'caution', message: 'Nothing has been minted.' })\n if (info.decimals > 24) risks.push({ code: 'unusual-decimals', level: 'caution', message: `It uses ${info.decimals} decimals; amounts may display oddly.` })\n for (const flag of info.flags) {\n const r = FLAG_RISKS[flag]\n if (r) risks.push({ code: flag, ...r })\n }\n risks.sort((a, b) => (a.level === b.level ? 0 : a.level === 'danger' ? -1 : 1))\n return { level: risks.some((r) => r.level === 'danger') ? 'danger' : 'caution', risks }\n}\n","import type { IWalletAccountReadOnly, TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { catalogChainId, ccAssetByToken, isTokenAddress, type CcAsset } from '@anywalletkit/swap2-assets'\nimport { assessToken, type TokenAssessment } from './assess.js'\n\n/** One JSON value under one key — the shape every small store needs (AsyncStorage, chrome.storage, memory). */\nexport interface JsonStorage<T> {\n /** The stored value, or `null` on first run. */\n get(): Promise<T | null>\n /** Persist the value. */\n set(value: T): Promise<void>\n}\n\n/** In-memory storage for tests and smoke runs. */\nexport function memoryJsonStorage<T>(initial: T | null = null): JsonStorage<T> & { value: T | null } {\n const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T\n const s = {\n value: initial,\n async get() { return s.value === null ? null : clone(s.value) },\n async set(value: T) { s.value = clone(value) },\n }\n return s\n}\n\n/** A token the user added. `symbol` and `name` are what the token reports: display them, never match on them. */\nexport interface UserToken {\n /** CAIP-2 chain id, the catalog's key (`solana:mainnet`, `eip155:1`). */\n chain: string\n /** Contract (EVM, checksummed as read) or mint (Solana). The token's identity. */\n address: string\n symbol: string\n name: string\n decimals: number\n /** `erc20`, `spl` or `spl-token-2022`. */\n standard: string\n /** Facts read from the chain when it was added. */\n flags: TokenFlag[]\n /** When the user added it (ms since epoch). */\n addedAt: number\n}\n\n/** What {@link TokensTable} stores. */\nexport interface TokensState {\n v: 1\n tokens: UserToken[]\n}\n\n/** Codes of {@link TokensError}. */\nexport type TokensErrorCode = 'not-loaded' | 'unsupported-chain' | 'bad-address' | 'listed' | 'bad-token' | 'full'\n\n/** A refusal from the tokens table or {@link inspectToken}, with a machine-readable `code`. */\nexport class TokensError extends Error {\n readonly code: TokensErrorCode\n constructor(code: TokensErrorCode, message: string) {\n super(message)\n this.name = 'TokensError'\n this.code = code\n }\n}\n\nconst sameAddress = (chain: string, a: string, b: string) => (chain.startsWith('eip155:') ? a.toLowerCase() === b.toLowerCase() : a === b)\nconst FLAGS: ReadonlySet<string> = new Set<TokenFlag>(['upgradeable', 'mint-authority', 'freeze-authority', 'permanent-delegate', 'transfer-fee', 'transfer-hook', 'non-transferable', 'default-frozen', 'pausable'])\n\n/** A stored entry as it must be, or null (a hand-edited or corrupted entry is dropped, never trusted). */\nfunction validEntry(t: unknown): UserToken | null {\n if (!t || typeof t !== 'object') return null\n const e = t as Record<string, unknown>\n if (typeof e.chain !== 'string' || typeof e.address !== 'string' || !isTokenAddress(e.chain, e.address)) return null\n if (typeof e.decimals !== 'number' || !Number.isInteger(e.decimals) || e.decimals < 0 || e.decimals > 77) return null\n if (typeof e.symbol !== 'string' || typeof e.name !== 'string' || typeof e.standard !== 'string' || typeof e.addedAt !== 'number') return null\n if (!Array.isArray(e.flags) || !e.flags.every((f) => typeof f === 'string' && FLAGS.has(f))) return null\n return {\n chain: e.chain, address: e.address, symbol: e.symbol.slice(0, 128), name: e.name.slice(0, 128), decimals: e.decimals,\n standard: e.standard, flags: [...e.flags] as TokenFlag[], addedAt: e.addedAt,\n }\n}\n\n/** The result of looking a token up before adding it. */\nexport type TokenInspection =\n | { listed: CcAsset }\n | { listed?: undefined; info: TokenInfo; assessment: TokenAssessment; token: UserToken }\n\n/**\n * Read a token from the chain through `account` (its chain decides where) and assess it. A token the\n * catalog already lists comes back as `{ listed }`: nothing to add. Otherwise the token, ready for\n * {@link TokensTable.add}, and the warnings to show first. Throws {@link TokensError}\n * (`unsupported-chain`, `bad-address`) or the chain package's `NotATokenError`.\n */\nexport async function inspectToken(\n account: Pick<IWalletAccountReadOnly, 'chain' | 'getTokenInfo'> & { getChainId?: () => Promise<number | bigint> },\n address: string,\n now: () => number = Date.now,\n): Promise<TokenInspection> {\n let reported = account.chain\n // An EVM account created without a chain id reports 'eip155:unknown' until it has asked its RPC:\n // ask now, or the catalog check (already listed? a look-alike of this chain's tokens?) would miss.\n if (reported === 'eip155:unknown') {\n if (typeof account.getChainId !== 'function') throw new TokensError('unsupported-chain', 'The account does not know its chain id; create it with chainId.')\n reported = `eip155:${String(await account.getChainId())}`\n }\n const chain = catalogChainId(reported)\n if (!chain.startsWith('eip155:') && !chain.startsWith('solana:')) throw new TokensError('unsupported-chain', `Adding tokens is supported on EVM chains and Solana, not ${account.chain}.`)\n const ref = address.trim()\n if (!isTokenAddress(chain, ref)) throw new TokensError('bad-address', `${JSON.stringify(address)} is not a token address on ${chain}.`)\n const listed = ccAssetByToken(chain, ref)\n if (listed) return { listed }\n if (typeof account.getTokenInfo !== 'function') throw new TokensError('unsupported-chain', `This ${account.chain} account cannot read token info.`)\n const info = await account.getTokenInfo(ref)\n const token: UserToken = {\n chain, address: info.address, symbol: info.symbol.slice(0, 128), name: info.name.slice(0, 128), decimals: info.decimals,\n standard: info.standard, flags: [...info.flags], addedAt: now(),\n }\n return { info, assessment: assessToken(info, chain), token }\n}\n\n/**\n * The swap options that name a user token as the source: its address and decimals. Pass them to an\n * engine created with `allowUnlistedTokens: true`.\n */\nexport function swapSource(token: Pick<UserToken, 'address' | 'decimals'>): { fromToken: string; fromTokenDecimals: number } {\n return { fromToken: token.address, fromTokenDecimals: token.decimals }\n}\n\n/**\n * The user's own tokens, over any JSON storage: load once at cold start, then read synchronously.\n * Writes queue behind each other; a failed save leaves memory as stored. Tokens the catalog lists are\n * refused (they are already there), and so are addresses that are not EVM contracts or Solana mints.\n */\nexport class TokensTable {\n private _state: TokensState | null = null\n private _loading: Promise<TokensState> | null = null\n private _writes: Promise<unknown> = Promise.resolve()\n private readonly listeners = new Set<() => void>()\n readonly maxTokens: number\n\n constructor(private readonly storage: JsonStorage<TokensState>, options: { maxTokens?: number } = {}) {\n this.maxTokens = options.maxTokens ?? 200\n }\n\n /** Read the stored list. Call once before the sync reads; a failed read throws and leaves the table unloaded. */\n async load(): Promise<void> {\n await this.state()\n }\n\n /** Called after every successful write. Returns the unsubscribe function. */\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => { this.listeners.delete(fn) }\n }\n\n /** The user's tokens, oldest first; only those on `chain` when given. */\n list(chain?: string): UserToken[] {\n const s = this.loaded()\n const want = chain === undefined ? undefined : catalogChainId(chain)\n return s.tokens.filter((t) => want === undefined || t.chain === want).map((t) => ({ ...t, flags: [...t.flags] }))\n }\n\n /** The user token at `address` on `chain`, if added. */\n get(chain: string, address: string): UserToken | undefined {\n const c = catalogChainId(chain)\n const t = this.loaded().tokens.find((x) => x.chain === c && sameAddress(c, x.address, address))\n return t ? { ...t, flags: [...t.flags] } : undefined\n }\n\n /** Add (or refresh) a token — usually `inspection.token` after the user accepted the warnings. */\n async add(token: UserToken): Promise<UserToken> {\n const entry = validEntry({ ...token, chain: catalogChainId(token.chain) })\n if (!entry) throw new TokensError('bad-token', 'Not a valid token entry (address, decimals 0–77, symbol, name, standard, flags).')\n if (ccAssetByToken(entry.chain, entry.address)) throw new TokensError('listed', `${entry.address} is already in the wallet's token list.`)\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === entry.chain && sameAddress(entry.chain, t.address, entry.address)))\n if (rest.length >= this.maxTokens) throw new TokensError('full', `At most ${this.maxTokens} tokens can be added.`)\n return { v: 1, tokens: [...rest, entry] }\n })\n return { ...entry, flags: [...entry.flags] }\n }\n\n /** Remove a token. Returns whether it was there. */\n async remove(chain: string, address: string): Promise<boolean> {\n const c = catalogChainId(chain)\n let removed = false\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === c && sameAddress(c, t.address, address)))\n removed = rest.length !== s.tokens.length\n return { v: 1, tokens: rest }\n })\n return removed\n }\n\n private loaded(): TokensState {\n if (!this._state) throw new TokensError('not-loaded', 'Call load() before reading the tokens table.')\n return this._state\n }\n\n private async state(): Promise<TokensState> {\n if (this._state) return this._state\n this._loading ??= (async () => {\n let stored: TokensState | null\n try {\n stored = await this.storage.get()\n } catch (e) {\n // A failed read is not an empty list: the next write would erase the user's tokens.\n this._loading = null\n throw e\n }\n const tokens = stored && stored.v === 1 && Array.isArray(stored.tokens)\n ? stored.tokens.map(validEntry).filter((t): t is UserToken => t !== null)\n : []\n this._state = { v: 1, tokens }\n return this._state\n })()\n return this._loading\n }\n\n private update(change: (s: TokensState) => TokensState): Promise<void> {\n const run = this._writes.then(async () => {\n const next = change(await this.state())\n await this.storage.set(next)\n this._state = next\n for (const fn of this.listeners) {\n try { fn() } catch { /* a listener's failure is its own */ }\n }\n })\n this._writes = run.catch(() => undefined)\n return run\n }\n}\n"],"mappings":"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","names":["catalogChainId","catalogChainId"]}
|
|
1
|
+
{"version":3,"sources":["../src/assess.ts","../src/table.ts"],"sourcesContent":["import type { TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { CC_SOURCE_CHAINS, catalogChainId, ccChain } from '@anywalletkit/swap2-assets'\n\n/** `danger`: the token can take or trap the user's money, or pretends to be another one. `caution`: worth knowing. */\nexport type TokenRiskLevel = 'danger' | 'caution'\n\n/**\n * Why a token deserves a warning:\n * - `unlisted`: always — the wallet's list does not include it, and anyone can deploy a token with any name and logo.\n * - `impersonation`: its symbol reads as a known token's (USDC, ETH, …), look-alike letters included.\n * - `odd-characters`: its symbol or name uses characters outside plain ASCII, or invisible ones.\n * - `no-symbol`: it reports no symbol.\n * - `zero-supply`: nothing has been minted.\n * - `unusual-decimals`: more than 24 decimals.\n * - a {@link TokenFlag} read from the chain (issuer powers, Token-2022 extensions).\n */\nexport type TokenRiskCode = 'unlisted' | 'impersonation' | 'odd-characters' | 'no-symbol' | 'zero-supply' | 'unusual-decimals' | TokenFlag\n\n/** One warning, worded for the person adding the token. */\nexport interface TokenRisk {\n code: TokenRiskCode\n level: TokenRiskLevel\n message: string\n}\n\n/** Every warning that applies, worst first, and the worst level. A user token is never \"safe\": `caution` is the floor. */\nexport interface TokenAssessment {\n level: TokenRiskLevel\n risks: TokenRisk[]\n}\n\nconst FLAG_RISKS: Readonly<Record<TokenFlag, { level: TokenRiskLevel; message: string }>> = {\n 'upgradeable': { level: 'caution', message: \"The token's owner can replace its code.\" },\n 'mint-authority': { level: 'caution', message: 'The issuer can mint more at any time.' },\n 'freeze-authority': { level: 'caution', message: 'The issuer can freeze your balance.' },\n 'permanent-delegate': { level: 'danger', message: 'The issuer can move or burn the tokens in your wallet.' },\n 'transfer-fee': { level: 'caution', message: 'Every transfer pays a fee to the issuer.' },\n 'transfer-hook': { level: 'danger', message: \"A program of the issuer's choosing runs on every transfer: it can block selling.\" },\n 'non-transferable': { level: 'danger', message: 'It cannot be moved: not sent, not swapped.' },\n 'default-frozen': { level: 'caution', message: 'New holders start frozen until the issuer lets them move it.' },\n 'pausable': { level: 'caution', message: 'The issuer can pause every transfer.' },\n}\n\n/** Longest symbol the wallet stores and assesses (code points); a longer one is cut, after trimming, before anything reads it. */\nexport const TOKEN_SYMBOL_MAX = 32\n/** Longest name the wallet stores (code points). */\nexport const TOKEN_NAME_MAX = 128\n\n/**\n * A symbol or name as the wallet keeps and shows it: trimmed, cut to `max` code points, trimmed again —\n * so \"USDC\" + 124 spaces + \"X\" is stored, shown and assessed as \"USDC\" (scan 2026-10-27 modules, F28).\n */\nexport function tokenText(text: string, max: number): string {\n let out = ''\n let n = 0\n for (const ch of text.trim()) {\n if (n++ >= max) break\n out += ch\n }\n return out.trim()\n}\n\n/** Letters that render like Latin capitals (Cyrillic, Greek) and currency signs used as letters. */\nconst CONFUSABLES: Readonly<Record<string, string>> = {\n 'А': 'A', 'В': 'B', 'Е': 'E', 'К': 'K', 'М': 'M', 'Н': 'H', 'О': 'O', 'Р': 'P', 'С': 'C', 'Т': 'T', 'Х': 'X', 'У': 'Y', 'І': 'I', 'Ј': 'J', 'Ѕ': 'S', 'Ԁ': 'D',\n 'а': 'A', 'в': 'B', 'е': 'E', 'к': 'K', 'м': 'M', 'н': 'H', 'о': 'O', 'р': 'P', 'с': 'C', 'т': 'T', 'х': 'X', 'у': 'Y', 'і': 'I', 'ј': 'J', 'ѕ': 'S', 'ԁ': 'D',\n 'Α': 'A', 'Β': 'B', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Ι': 'I', 'Κ': 'K', 'Μ': 'M', 'Ν': 'N', 'Ο': 'O', 'Ρ': 'P', 'Τ': 'T', 'Υ': 'Y', 'Χ': 'X',\n 'α': 'A', 'ε': 'E', 'ι': 'I', 'κ': 'K', 'ν': 'V', 'ο': 'O', 'ρ': 'P', 'τ': 'T', 'υ': 'U', 'χ': 'X',\n '$': 'S', '₮': 'T', '€': 'E', '¢': 'C', '₿': 'B', 'Ξ': 'E',\n // ASCII and Latin look-alikes of I and O (UTS #39 confusables): \"DAl\" reads as DAI, \"USDl\" as USD1.\n 'l': 'I', '1': 'I', '|': 'I', 'ı': 'I', 'ł': 'I', 'ǀ': 'I', '0': 'O', 'ø': 'O', 'Ø': 'O',\n // Latin letters without a decomposition, and Latin-1 accented letters for runtimes without String.prototype.normalize.\n 'Ð': 'D', 'ð': 'D', 'Đ': 'D', 'đ': 'D', 'Ħ': 'H', 'ħ': 'H', 'Ŧ': 'T', 'ŧ': 'T', 'ß': 'B', 'Þ': 'P', 'þ': 'P', 'Ŋ': 'N', 'ŋ': 'N',\n 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'à': 'A', 'á': 'A', 'â': 'A', 'ã': 'A', 'ä': 'A', 'å': 'A',\n 'Ç': 'C', 'ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'è': 'E', 'é': 'E', 'ê': 'E', 'ë': 'E',\n 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I', 'ì': 'I', 'í': 'I', 'î': 'I', 'ï': 'I', 'Ñ': 'N', 'ñ': 'N',\n 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 'O', 'ò': 'O', 'ó': 'O', 'ô': 'O', 'õ': 'O', 'ö': 'O',\n 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'ù': 'U', 'ú': 'U', 'û': 'U', 'ü': 'U', 'Ý': 'Y', 'ý': 'Y', 'ÿ': 'Y',\n}\n/** Letter pairs that read as one letter (UTS #39): rn → m, cl → d, vv → w. */\nconst PAIRS: ReadonlyArray<[RegExp, string]> = [[/rn/g, 'm'], [/cl/g, 'd'], [/vv|VV/g, 'W']]\n/** Combining marks: an accent over a Latin letter reads as the letter. */\nconst MARKS = /[\\u0300-\\u036f\\u1ab0-\\u1aff\\u1dc0-\\u1dff\\u20d0-\\u20ff\\ufe20-\\ufe2f]/g\n/**\n * Characters a reader does not see: zero-width, bidi and other format characters, C0 / C1 controls,\n * fillers and blank glyphs (Hangul fillers, U+2800 braille blank), interlinear annotation, and the\n * astral ones — tag characters (ASCII smuggling), variation selectors 17–256, musical and shorthand\n * format controls. Explicit ranges: Hermes has no Unicode property escapes.\n */\nconst INVISIBLE = /[\\x00-\\x08\\x0E-\\x1F\\x7F-\\x9F\\u00AD\\u034F\\u061C\\u115F\\u1160\\u17B4\\u17B5\\u180B-\\u180F\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u2800\\u3164\\uFE00-\\uFE0F\\uFEFF\\uFFA0\\uFFF9-\\uFFFB]|\\uDB40[\\uDC00-\\uDC7F\\uDD00-\\uDDEF]|\\uD834[\\uDD59\\uDD73-\\uDD7A]|\\uD82F[\\uDCA0-\\uDCA3]/g\n\n/**\n * A symbol folded the way a reader sees it — a confusables skeleton in the spirit of UTS #39: invisible\n * characters and spaces gone, compatibility forms unified and accents dropped (NFKD), look-alike letters\n * mapped (Cyrillic, Greek, currency signs, and ASCII l / 1 / | → I, 0 → O; rn → m, cl → d, vv → w), upper\n * case. Only the first {@link TOKEN_SYMBOL_MAX} code points are read (scan 2026-10-27 modules, F36).\n */\nexport function foldSymbol(text: string): string {\n let s = tokenText(text, TOKEN_SYMBOL_MAX).replace(INVISIBLE, '')\n if (typeof s.normalize === 'function') s = s.normalize('NFKD').replace(MARKS, '')\n // After NFKD: a spacing accent (´ ¨ ¸ ‾) decomposes into a space and a mark.\n s = s.replace(/\\s+/g, '')\n // Fullwidth ASCII by hand too: some Hermes builds ship without String.prototype.normalize.\n s = s.replace(/[\\uFF01-\\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))\n for (const [re, to] of PAIRS) s = s.replace(re, to)\n let out = ''\n for (const ch of s) out += CONFUSABLES[ch] ?? ch\n return out.toUpperCase()\n}\n\n/** Symbols nobody should reuse on any chain: the big stablecoins and wrapped coins (natives are added from the catalog). */\nconst MAJOR_TOKENS = ['USDT', 'USDC', 'DAI', 'USDe', 'USDS', 'PYUSD', 'FDUSD', 'USD1', 'USDG', 'EURC', 'WETH', 'WBTC', 'cbBTC', 'stETH', 'wstETH', 'WSOL']\n\nlet majors: ReadonlyMap<string, string> | undefined\n/** Folded → display: every chain's native coin from the catalog, plus {@link MAJOR_TOKENS}. */\nfunction majorSymbols(): ReadonlyMap<string, string> {\n if (majors) return majors\n const m = new Map<string, string>()\n for (const chain of CC_SOURCE_CHAINS) for (const a of chain.assets) if (!a.contract && !m.has(foldSymbol(a.symbol))) m.set(foldSymbol(a.symbol), a.symbol)\n for (const sym of MAJOR_TOKENS) if (!m.has(foldSymbol(sym))) m.set(foldSymbol(sym), sym)\n majors = m\n return m\n}\n\n/** The known token a folded symbol reads as: any catalog token on the same chain, or a major one anywhere. */\nfunction lookalikeOf(folded: string, chain: string | undefined): string | undefined {\n if (!folded) return undefined\n if (chain) {\n const same = ccChain(catalogChainId(chain))?.assets.find((a) => foldSymbol(a.symbol) === folded)\n if (same) return same.symbol\n }\n return majorSymbols().get(folded)\n}\n\nconst PLAIN = /^[\\x20-\\x7E]*$/\n/**\n * Letters that pass for Latin ones, by block (the whole block, as for Cyrillic): IPA and spacing\n * modifier letters, Greek, Cyrillic, Armenian, Cherokee, Canadian syllabics, runic, phonetic letters\n * and small capitals (also Latin Extended-D / E), letterlike symbols (ℂ ℍ ℕ — not ™ ℠ ℗), Roman\n * numerals, enclosed letters and digits (Ⓤ ①, 🅤), Coptic, Tifinagh, Lisu, Vai, mathematical letters\n * (𝐔) and the astral Deseret, Osage, Lycian, Carian, Old Italic and Warang Citi; for Georgian, a living\n * script, only its look-alike letters (Ⴝ ყ ჿ Ჽ ⴭ…). Explicit ranges: Hermes has no Unicode property\n * escapes. Emoji, CJK, Arabic, Hebrew, Thai and accented Latin are not here: a caution, not a danger.\n */\nconst LOOKALIKE_LETTERS = /[\\u0250-\\u02FF\\u0370-\\u03FF\\u0400-\\u052F\\u0530-\\u058F\\u10B9\\u10BD\\u10C7\\u10CD\\u10E7\\u10FD\\u10FF\\u13A0-\\u13FF\\u1400-\\u167F\\u16A0-\\u16FF\\u1C80-\\u1C8F\\u1C95\\u1CB7\\u1CBD\\u1CBF\\u1D00-\\u1DBF\\u1F00-\\u1FFF\\u2100-\\u2116\\u2118-\\u211F\\u2121\\u2123-\\u214F\\u2160-\\u2188\\u2460-\\u24FF\\u2C80-\\u2CFF\\u2D2D\\u2D30-\\u2D7F\\u2DE0-\\u2DFF\\uA4D0-\\uA4FF\\uA500-\\uA63F\\uA640-\\uA69F\\uA720-\\uA7FF\\uAB30-\\uABBF]|\\uD835[\\uDC00-\\uDFFF]|\\uD83C[\\uDD10-\\uDD69]|\\uD801[\\uDC00-\\uDC4F\\uDCB0-\\uDCFF]|\\uD800[\\uDE80-\\uDEDF\\uDF00-\\uDF2F]|\\uD806[\\uDCA0-\\uDCFF]/\n/**\n * What hides inside text: {@link INVISIBLE} without the joiner and the variation selectors 1–16\n * emoji are built with.\n */\nconst HIDING = /[\\x00-\\x08\\x0E-\\x1F\\x7F-\\x9F\\u00AD\\u034F\\u061C\\u115F\\u1160\\u17B4\\u17B5\\u180B-\\u180F\\u200B\\u200C\\u200E\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u2800\\u3164\\uFEFF\\uFFA0\\uFFF9-\\uFFFB]|\\uDB40[\\uDC00-\\uDC7F\\uDD00-\\uDDEF]|\\uD834[\\uDD59\\uDD73-\\uDD7A]|\\uD82F[\\uDCA0-\\uDCA3]/\n/** A line break inside a symbol: what follows it is on a line a one-line label does not show. */\nconst LINE_BREAK = /[\\n\\v\\f\\r\\u0085\\u2028\\u2029]/\n/** Emoji built from tag characters (the England, Scotland and Wales flags) are emoji, not hidden text. */\nconst EMOJI_TAG_SEQUENCE = /\\uD83C\\uDFF4(?:\\uDB40[\\uDC20-\\uDC7E])+\\uDB40\\uDC7F/g\n/** Zero-width non-joiner between two non-ASCII characters is spelling (Persian, Indic scripts), not hiding. */\nconst SPELLING_ZWNJ = /([^\\x00-\\x7F])\\u200C(?=[^\\x00-\\x7F])/g\n/** Letters that pass for Latin ones, or characters that hide, in `text`. */\nconst passesOrHides = (text: string): boolean => LOOKALIKE_LETTERS.test(text) || HIDING.test(text.replace(EMOJI_TAG_SEQUENCE, '').replace(SPELLING_ZWNJ, '$1'))\n\n\n/**\n * Warnings for a token read with `getTokenInfo`, for a token the wallet's list does not include.\n * Pure: nothing is fetched. `impersonation` when its symbol (look-alike letters folded) reads as a\n * catalog token on the same `chain` — the real one would be in the list, this is another contract —\n * or as a native coin or major stablecoin anywhere. A bridged token of another chain's list (JUP on\n * Solana, listed on Unichain) is not flagged.\n */\nexport function assessToken(info: TokenInfo, chain?: string): TokenAssessment {\n const risks: TokenRisk[] = [{ code: 'unlisted', level: 'caution', message: \"Not in the wallet's token list. Anyone can create a token with any name and logo: check the address with the project itself.\" }]\n // What the wallet stores and shows (trimmed, cut to TOKEN_SYMBOL_MAX), and the whole text a UI that does\n // not cut it would show: a look-alike in either is one (scan 2026-10-27 modules, F28).\n const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX)\n const name = tokenText(info.name, TOKEN_NAME_MAX)\n const whole = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX).replace(/\\s+/g, '')\n const lookalike = lookalikeOf(foldSymbol(symbol), chain) ?? lookalikeOf(foldSymbol(whole), chain)\n if (lookalike) {\n risks.push({ code: 'impersonation', level: 'danger', message: `It calls itself ${JSON.stringify(symbol)}, like ${lookalike}, but it is a different token at a different address.` })\n }\n // The raw head (what a host showing the raw symbol shows) and the stored symbol (what this wallet shows):\n // leading spaces must not push the look-alike out of the part that is checked.\n const head = info.symbol.slice(0, 4 * TOKEN_SYMBOL_MAX)\n if (passesOrHides(head) || passesOrHides(symbol) || LINE_BREAK.test(symbol)) {\n // Letters of another alphabet that pass for Latin ones, or characters that hide, are how a token passes for a\n // known one (scan 2026-10-27 modules, F36). Emoji, CJK and accented Latin stay a caution (owner decision 2026-10-28).\n risks.push({ code: 'odd-characters', level: 'danger', message: 'Its symbol uses letters of another alphabet that pass for Latin ones (Cyrillic, Greek…) or characters that hide in text.' })\n } else if (!PLAIN.test(symbol) || /\\s/.test(symbol) || !PLAIN.test(name) || name !== name.replace(INVISIBLE, '')) {\n risks.push({ code: 'odd-characters', level: 'caution', message: 'Its symbol or name uses unusual characters (emoji, other scripts, accents), spaces inside the symbol, or invisible ones.' })\n }\n if (!symbol) risks.push({ code: 'no-symbol', level: 'caution', message: 'The token reports no symbol.' })\n if (info.totalSupply === 0n) risks.push({ code: 'zero-supply', level: 'caution', message: 'Nothing has been minted.' })\n if (info.decimals > 24) risks.push({ code: 'unusual-decimals', level: 'caution', message: `It uses ${info.decimals} decimals; amounts may display oddly.` })\n for (const flag of info.flags) {\n const r = FLAG_RISKS[flag]\n if (r) risks.push({ code: flag, ...r })\n }\n risks.sort((a, b) => (a.level === b.level ? 0 : a.level === 'danger' ? -1 : 1))\n return { level: risks.some((r) => r.level === 'danger') ? 'danger' : 'caution', risks }\n}\n","import type { IWalletAccountReadOnly, TokenFlag, TokenInfo } from '@anywalletkit/core'\nimport { catalogChainId, ccAssetByToken, isTokenAddress, type CcAsset } from '@anywalletkit/swap2-assets'\nimport { TOKEN_NAME_MAX, TOKEN_SYMBOL_MAX, assessToken, tokenText, type TokenAssessment } from './assess.js'\n\n/** One JSON value under one key — the shape every small store needs (AsyncStorage, chrome.storage, memory). */\nexport interface JsonStorage<T> {\n /** The stored value, or `null` on first run. */\n get(): Promise<T | null>\n /** Persist the value. */\n set(value: T): Promise<void>\n}\n\n/** In-memory storage for tests and smoke runs. */\nexport function memoryJsonStorage<T>(initial: T | null = null): JsonStorage<T> & { value: T | null } {\n const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T\n const s = {\n value: initial,\n async get() { return s.value === null ? null : clone(s.value) },\n async set(value: T) { s.value = clone(value) },\n }\n return s\n}\n\n/** A token the user added. `symbol` and `name` are what the token reports: display them, never match on them. */\nexport interface UserToken {\n /** CAIP-2 chain id, the catalog's key (`solana:mainnet`, `eip155:1`). */\n chain: string\n /** Contract (EVM, checksummed as read) or mint (Solana). The token's identity. */\n address: string\n symbol: string\n name: string\n decimals: number\n /** `erc20`, `spl` or `spl-token-2022`. */\n standard: string\n /** Facts read from the chain when it was added. */\n flags: TokenFlag[]\n /** When the user added it (ms since epoch). */\n addedAt: number\n}\n\n/** What {@link TokensTable} stores. */\nexport interface TokensState {\n v: 1\n tokens: UserToken[]\n}\n\n/** Codes of {@link TokensError}. */\nexport type TokensErrorCode = 'not-loaded' | 'unsupported-chain' | 'bad-address' | 'listed' | 'bad-token' | 'full'\n\n/** A refusal from the tokens table or {@link inspectToken}, with a machine-readable `code`. */\nexport class TokensError extends Error {\n readonly code: TokensErrorCode\n constructor(code: TokensErrorCode, message: string) {\n super(message)\n this.name = 'TokensError'\n this.code = code\n }\n}\n\nconst sameAddress = (chain: string, a: string, b: string) => (chain.startsWith('eip155:') ? a.toLowerCase() === b.toLowerCase() : a === b)\nconst FLAGS: ReadonlySet<string> = new Set<TokenFlag>(['upgradeable', 'mint-authority', 'freeze-authority', 'permanent-delegate', 'transfer-fee', 'transfer-hook', 'non-transferable', 'default-frozen', 'pausable'])\n\n/** A stored entry as it must be, or null (a hand-edited or corrupted entry is dropped, never trusted). */\nfunction validEntry(t: unknown): UserToken | null {\n if (!t || typeof t !== 'object') return null\n const e = t as Record<string, unknown>\n if (typeof e.chain !== 'string' || typeof e.address !== 'string' || !isTokenAddress(e.chain, e.address)) return null\n if (typeof e.decimals !== 'number' || !Number.isInteger(e.decimals) || e.decimals < 0 || e.decimals > 77) return null\n if (typeof e.symbol !== 'string' || typeof e.name !== 'string' || typeof e.standard !== 'string' || typeof e.addedAt !== 'number') return null\n if (!Array.isArray(e.flags) || !e.flags.every((f) => typeof f === 'string' && FLAGS.has(f))) return null\n return {\n chain: e.chain, address: e.address, symbol: tokenText(e.symbol, TOKEN_SYMBOL_MAX), name: tokenText(e.name, TOKEN_NAME_MAX), decimals: e.decimals,\n standard: e.standard, flags: [...e.flags] as TokenFlag[], addedAt: e.addedAt,\n }\n}\n\n/** The result of looking a token up before adding it. */\nexport type TokenInspection =\n | { listed: CcAsset }\n | { listed?: undefined; info: TokenInfo; assessment: TokenAssessment; token: UserToken }\n\n/**\n * Read a token from the chain through `account` (its chain decides where) and assess it. A token the\n * catalog already lists comes back as `{ listed }`: nothing to add. Otherwise the token, ready for\n * {@link TokensTable.add}, and the warnings to show first. Throws {@link TokensError}\n * (`unsupported-chain`, `bad-address`) or the chain package's `NotATokenError`.\n */\nexport async function inspectToken(\n account: Pick<IWalletAccountReadOnly, 'chain' | 'getTokenInfo'> & { getChainId?: () => Promise<number | bigint> },\n address: string,\n now: () => number = Date.now,\n): Promise<TokenInspection> {\n let reported = account.chain\n // An EVM account created without a chain id reports 'eip155:unknown' until it has asked its RPC:\n // ask now, or the catalog check (already listed? a look-alike of this chain's tokens?) would miss.\n if (reported === 'eip155:unknown') {\n if (typeof account.getChainId !== 'function') throw new TokensError('unsupported-chain', 'The account does not know its chain id; create it with chainId.')\n reported = `eip155:${String(await account.getChainId())}`\n }\n const chain = catalogChainId(reported)\n if (!chain.startsWith('eip155:') && !chain.startsWith('solana:')) throw new TokensError('unsupported-chain', `Adding tokens is supported on EVM chains and Solana, not ${account.chain}.`)\n const ref = address.trim()\n if (!isTokenAddress(chain, ref)) throw new TokensError('bad-address', `${JSON.stringify(address)} is not a token address on ${chain}.`)\n const listed = ccAssetByToken(chain, ref)\n if (listed) return { listed }\n if (typeof account.getTokenInfo !== 'function') throw new TokensError('unsupported-chain', `This ${account.chain} account cannot read token info.`)\n const info = await account.getTokenInfo(ref)\n // What is stored is what is assessed: trimmed and cut first (scan 2026-10-27 modules, F28).\n const symbol = tokenText(info.symbol, TOKEN_SYMBOL_MAX)\n const name = tokenText(info.name, TOKEN_NAME_MAX)\n const token: UserToken = {\n chain, address: info.address, symbol, name, decimals: info.decimals,\n standard: info.standard, flags: [...info.flags], addedAt: now(),\n }\n return { info, assessment: assessToken({ ...info, symbol, name }, chain), token }\n}\n\n/**\n * The swap options that name a user token as the source: its address and decimals. Pass them to an\n * engine created with `allowUnlistedTokens: true`.\n */\nexport function swapSource(token: Pick<UserToken, 'address' | 'decimals'>): { fromToken: string; fromTokenDecimals: number } {\n return { fromToken: token.address, fromTokenDecimals: token.decimals }\n}\n\n/**\n * The user's own tokens, over any JSON storage: load once at cold start, then read synchronously.\n * Writes queue behind each other; a failed save leaves memory as stored. Tokens the catalog lists are\n * refused (they are already there), and so are addresses that are not EVM contracts or Solana mints.\n */\nexport class TokensTable {\n private _state: TokensState | null = null\n private _loading: Promise<TokensState> | null = null\n private _writes: Promise<unknown> = Promise.resolve()\n private readonly listeners = new Set<() => void>()\n readonly maxTokens: number\n\n constructor(private readonly storage: JsonStorage<TokensState>, options: { maxTokens?: number } = {}) {\n this.maxTokens = options.maxTokens ?? 200\n }\n\n /** Read the stored list. Call once before the sync reads; a failed read throws and leaves the table unloaded. */\n async load(): Promise<void> {\n await this.state()\n }\n\n /** Called after every successful write. Returns the unsubscribe function. */\n subscribe(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => { this.listeners.delete(fn) }\n }\n\n /** The user's tokens, oldest first; only those on `chain` when given. */\n list(chain?: string): UserToken[] {\n const s = this.loaded()\n const want = chain === undefined ? undefined : catalogChainId(chain)\n return s.tokens.filter((t) => want === undefined || t.chain === want).map((t) => ({ ...t, flags: [...t.flags] }))\n }\n\n /** The user token at `address` on `chain`, if added. */\n get(chain: string, address: string): UserToken | undefined {\n const c = catalogChainId(chain)\n const t = this.loaded().tokens.find((x) => x.chain === c && sameAddress(c, x.address, address))\n return t ? { ...t, flags: [...t.flags] } : undefined\n }\n\n /** Add (or refresh) a token — usually `inspection.token` after the user accepted the warnings. */\n async add(token: UserToken): Promise<UserToken> {\n const entry = validEntry({ ...token, chain: catalogChainId(token.chain) })\n if (!entry) throw new TokensError('bad-token', 'Not a valid token entry (address, decimals 0–77, symbol, name, standard, flags).')\n if (ccAssetByToken(entry.chain, entry.address)) throw new TokensError('listed', `${entry.address} is already in the wallet's token list.`)\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === entry.chain && sameAddress(entry.chain, t.address, entry.address)))\n if (rest.length >= this.maxTokens) throw new TokensError('full', `At most ${this.maxTokens} tokens can be added.`)\n return { v: 1, tokens: [...rest, entry] }\n })\n return { ...entry, flags: [...entry.flags] }\n }\n\n /** Remove a token. Returns whether it was there. */\n async remove(chain: string, address: string): Promise<boolean> {\n const c = catalogChainId(chain)\n let removed = false\n await this.update((s) => {\n const rest = s.tokens.filter((t) => !(t.chain === c && sameAddress(c, t.address, address)))\n removed = rest.length !== s.tokens.length\n return { v: 1, tokens: rest }\n })\n return removed\n }\n\n private loaded(): TokensState {\n if (!this._state) throw new TokensError('not-loaded', 'Call load() before reading the tokens table.')\n return this._state\n }\n\n private async state(): Promise<TokensState> {\n if (this._state) return this._state\n this._loading ??= (async () => {\n let stored: TokensState | null\n try {\n stored = await this.storage.get()\n } catch (e) {\n // A failed read is not an empty list: the next write would erase the user's tokens.\n this._loading = null\n throw e\n }\n const tokens = stored && stored.v === 1 && Array.isArray(stored.tokens)\n ? stored.tokens.map(validEntry).filter((t): t is UserToken => t !== null)\n : []\n this._state = { v: 1, tokens }\n return this._state\n })()\n return this._loading\n }\n\n private update(change: (s: TokensState) => TokensState): Promise<void> {\n const run = this._writes.then(async () => {\n const next = change(await this.state())\n await this.storage.set(next)\n this._state = next\n for (const fn of this.listeners) {\n try { fn() } catch { /* a listener's failure is its own */ }\n }\n })\n this._writes = run.catch(() => undefined)\n return run\n }\n}\n"],"mappings":"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","names":["catalogChainId","catalogChainId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anywalletkit/tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Tokens users add themselves, at their own risk: read a token's decimals, symbol, name and holder-relevant facts from the chain (EVM contracts, Solana mints incl. Token-2022 extensions), a risk assessment that flags look-alikes of known tokens and issuer powers, and a persistent list (TokensTable over any JSON storage) that apps show beside the swap catalog and can swap through the engines that accept unlisted tokens.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://docs.anywallet.tech",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@types/node": "^22",
|
|
43
43
|
"vitest": "^5.0.3",
|
|
44
44
|
"@anywalletkit/core": "0.6.1",
|
|
45
|
-
"@anywalletkit/wallet-
|
|
46
|
-
"@anywalletkit/wallet-
|
|
45
|
+
"@anywalletkit/wallet-solana": "0.4.0",
|
|
46
|
+
"@anywalletkit/wallet-evm": "0.4.1"
|
|
47
47
|
},
|
|
48
48
|
"keywords": [
|
|
49
49
|
"tokens",
|