solana-risk-engine 0.4.4 → 0.4.6

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/src/client.js CHANGED
@@ -1,157 +1,157 @@
1
- import { PUBLIC_RPC, VIBE_RPC } from "./constants.js";
2
- import { ScanError, cancelled, isCancel } from "./errors.js";
3
- import { sleep } from "./util.js";
4
-
5
- // Methods PublicNode rejects with 403; if another one shows up it gets added at runtime
6
- const PN_BLOCKED_DEFAULT = ["getTokenLargestAccounts", "getTokenAccountsByOwner", "getTokenSupply"];
7
-
8
- /**
9
- * Creates the network client every engine function uses.
10
- *
11
- * @param {object} [opts]
12
- * @param {string} [opts.rpcUrl] your own RPC URL (Helius, Triton...). Without it the free
13
- * PublicNode + Solana Vibe Station combination is used.
14
- * @param {string} [opts.heliusApiKey] Helius key for PnL. If omitted it is taken from rpcUrl when
15
- * that URL is a Helius one (the key is never sent anywhere else).
16
- * @param {AbortSignal} [opts.signal] aborts every pending request with a CANCELLED ScanError
17
- * @param {Function} [opts.fetch] fetch implementation (defaults to globalThis.fetch)
18
- * @param {number} [opts.retryDelayMs] first retry delay, doubled each time (default 1200)
19
- * @param {number} [opts.vibeIntervalMs] spacing of the free Vibe Station queue (default 1100)
20
- */
21
- export function createClient(opts) {
22
- opts = opts || {};
23
- const fetchImpl = opts.fetch || globalThis.fetch;
24
- if (!fetchImpl) throw new Error("no fetch available: pass opts.fetch or use Node 18+");
25
- const rpcUrl = (opts.rpcUrl || "").trim() || PUBLIC_RPC;
26
- const publicRpc = opts.publicRpc || PUBLIC_RPC;
27
- const vibeRpc = opts.vibeRpc || VIBE_RPC;
28
- const signal = opts.signal || null;
29
- const retryDelay = opts.retryDelayMs === undefined ? 1200 : opts.retryDelayMs;
30
- const vibeInterval = opts.vibeIntervalMs === undefined ? 1100 : opts.vibeIntervalMs;
31
- const blocked = {};
32
- PN_BLOCKED_DEFAULT.forEach(function (m) { blocked[m] = 1; });
33
- let vibeNext = 0;
34
-
35
- function checkAbort() {
36
- if (signal && signal.aborted) throw cancelled();
37
- }
38
- function ownRpc() {
39
- return rpcUrl !== publicRpc;
40
- }
41
- function heliusKey() {
42
- if (opts.heliusApiKey) return opts.heliusApiKey;
43
- // Only use the key if the configured RPC really is Helius: otherwise it is never sent to api.helius.xyz
44
- try {
45
- const u = new URL(rpcUrl);
46
- if (!/(^|\.)helius-rpc\.com$|(^|\.)helius\.xyz$/i.test(u.hostname)) return null;
47
- return u.searchParams.get("api-key") || null;
48
- } catch (e) { return null; }
49
- }
50
-
51
- async function rawFetch(url, init) {
52
- checkAbort();
53
- try {
54
- return await fetchImpl(url, signal ? Object.assign({}, init, { signal: signal }) : init);
55
- } catch (e) {
56
- if (signal && signal.aborted) throw cancelled();
57
- throw e;
58
- }
59
- }
60
-
61
- // JSON fetch with retries on 429, 5xx and network errors (doubling the wait each time)
62
- async function jfetch(url, init, tries) {
63
- let wait = retryDelay, last = null;
64
- tries = tries || 4;
65
- for (let i = 0; i < tries; i++) {
66
- checkAbort();
67
- try {
68
- const r = await rawFetch(url, init);
69
- if (r.status === 429 || r.status >= 500) {
70
- last = new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
71
- await sleep(wait); wait *= 2; continue;
72
- }
73
- if (!r.ok) throw new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
74
- return await r.json();
75
- } catch (e) {
76
- if (isCancel(e) || (e.code === "HTTP" && e.data.status >= 400 && e.data.status < 500)) throw e;
77
- last = e; await sleep(wait); wait *= 2;
78
- }
79
- }
80
- if (last && last.code) throw last;
81
- throw new ScanError("NETWORK", (last && last.message) || "no response", { cause: last && last.message });
82
- }
83
-
84
- // Fetch that never throws (except on cancel): used for optional extras such as RugCheck,
85
- // which must never block the main result if they fail, time out or are down
86
- async function softJson(url, timeoutMs) {
87
- checkAbort();
88
- const ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
89
- let onAbort = null;
90
- if (ctrl && signal) { onAbort = function () { ctrl.abort(); }; signal.addEventListener("abort", onAbort); }
91
- const to = ctrl ? setTimeout(function () { ctrl.abort(); }, timeoutMs || 8000) : null;
92
- try {
93
- const r = await fetchImpl(url, ctrl ? { signal: ctrl.signal } : undefined);
94
- if (!r.ok) return null;
95
- return await r.json();
96
- } catch (e) {
97
- return null;
98
- } finally {
99
- if (to) clearTimeout(to);
100
- if (onAbort) signal.removeEventListener("abort", onAbort);
101
- checkAbort();
102
- }
103
- }
104
-
105
- async function rpcAt(url, method, params, tries) {
106
- const j = await jfetch(url, {
107
- method: "POST",
108
- headers: { "Content-Type": "application/json" },
109
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: method, params: params })
110
- }, tries);
111
- if (j.error) throw new ScanError("RPC", j.error.message || "RPC error", { rpcCode: j.error.code });
112
- return j.result;
113
- }
114
-
115
- async function vibeSlot() {
116
- const now = Date.now(), at = Math.max(now, vibeNext);
117
- vibeNext = at + vibeInterval;
118
- if (at > now) await sleep(at - now);
119
- checkAbort();
120
- }
121
-
122
- async function rpc(method, params) {
123
- if (ownRpc()) return rpcAt(rpcUrl, method, params);
124
- // PublicNode accepts at most 10 accounts per getMultipleAccounts: split and merge the result
125
- if (method === "getMultipleAccounts" && params[0].length > 10) {
126
- const parts = [];
127
- for (let i = 0; i < params[0].length; i += 10) parts.push(rpc(method, [params[0].slice(i, i + 10)].concat(params.slice(1))));
128
- const rs = await Promise.all(parts);
129
- return { context: rs[0].context, value: [].concat.apply([], rs.map(function (r) { return r.value; })) };
130
- }
131
- if (!blocked[method]) {
132
- try {
133
- const res = await rpcAt(publicRpc, method, params);
134
- // PublicNode only keeps a few days of history: a list that ends before filling the page, or a
135
- // transaction it cannot find, may just be old. Vibe Station keeps it all, so it gets the last word
136
- if (method === "getSignaturesForAddress" && Array.isArray(res) && res.length < ((params[1] && params[1].limit) || 1000)) {
137
- await vibeSlot();
138
- const full = await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
139
- return Array.isArray(full) && full.length > res.length ? full : res;
140
- }
141
- if (method === "getTransaction" && res === null) {
142
- await vibeSlot();
143
- return await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
144
- }
145
- return res;
146
- }
147
- catch (e) {
148
- if (isCancel(e) || !(e.code === "HTTP" && (e.data.status === 401 || e.data.status === 403))) throw e;
149
- blocked[method] = 1;
150
- }
151
- }
152
- await vibeSlot();
153
- return rpcAt(vibeRpc, method, params, 6); // more retries: its limit sometimes trips even when the queue is respected
154
- }
155
-
156
- return { rpc: rpc, jfetch: jfetch, softJson: softJson, ownRpc: ownRpc, heliusKey: heliusKey, checkAbort: checkAbort, rpcUrl: rpcUrl, retryDelayMs: opts.retryDelayMs };
157
- }
1
+ import { PUBLIC_RPC, VIBE_RPC } from "./constants.js";
2
+ import { ScanError, cancelled, isCancel } from "./errors.js";
3
+ import { sleep } from "./util.js";
4
+
5
+ // Methods PublicNode rejects with 403; if another one shows up it gets added at runtime
6
+ const PN_BLOCKED_DEFAULT = ["getTokenLargestAccounts", "getTokenAccountsByOwner", "getTokenSupply"];
7
+
8
+ /**
9
+ * Creates the network client every engine function uses.
10
+ *
11
+ * @param {object} [opts]
12
+ * @param {string} [opts.rpcUrl] your own RPC URL (Helius, Triton...). Without it the free
13
+ * PublicNode + Solana Vibe Station combination is used.
14
+ * @param {string} [opts.heliusApiKey] Helius key for PnL. If omitted it is taken from rpcUrl when
15
+ * that URL is a Helius one (the key is never sent anywhere else).
16
+ * @param {AbortSignal} [opts.signal] aborts every pending request with a CANCELLED ScanError
17
+ * @param {Function} [opts.fetch] fetch implementation (defaults to globalThis.fetch)
18
+ * @param {number} [opts.retryDelayMs] first retry delay, doubled each time (default 1200)
19
+ * @param {number} [opts.vibeIntervalMs] spacing of the free Vibe Station queue (default 1100)
20
+ */
21
+ export function createClient(opts) {
22
+ opts = opts || {};
23
+ const fetchImpl = opts.fetch || globalThis.fetch;
24
+ if (!fetchImpl) throw new Error("no fetch available: pass opts.fetch or use Node 18+");
25
+ const rpcUrl = (opts.rpcUrl || "").trim() || PUBLIC_RPC;
26
+ const publicRpc = opts.publicRpc || PUBLIC_RPC;
27
+ const vibeRpc = opts.vibeRpc || VIBE_RPC;
28
+ const signal = opts.signal || null;
29
+ const retryDelay = opts.retryDelayMs === undefined ? 1200 : opts.retryDelayMs;
30
+ const vibeInterval = opts.vibeIntervalMs === undefined ? 1100 : opts.vibeIntervalMs;
31
+ const blocked = {};
32
+ PN_BLOCKED_DEFAULT.forEach(function (m) { blocked[m] = 1; });
33
+ let vibeNext = 0;
34
+
35
+ function checkAbort() {
36
+ if (signal && signal.aborted) throw cancelled();
37
+ }
38
+ function ownRpc() {
39
+ return rpcUrl !== publicRpc;
40
+ }
41
+ function heliusKey() {
42
+ if (opts.heliusApiKey) return opts.heliusApiKey;
43
+ // Only use the key if the configured RPC really is Helius: otherwise it is never sent to api.helius.xyz
44
+ try {
45
+ const u = new URL(rpcUrl);
46
+ if (!/(^|\.)helius-rpc\.com$|(^|\.)helius\.xyz$/i.test(u.hostname)) return null;
47
+ return u.searchParams.get("api-key") || null;
48
+ } catch (e) { return null; }
49
+ }
50
+
51
+ async function rawFetch(url, init) {
52
+ checkAbort();
53
+ try {
54
+ return await fetchImpl(url, signal ? Object.assign({}, init, { signal: signal }) : init);
55
+ } catch (e) {
56
+ if (signal && signal.aborted) throw cancelled();
57
+ throw e;
58
+ }
59
+ }
60
+
61
+ // JSON fetch with retries on 429, 5xx and network errors (doubling the wait each time)
62
+ async function jfetch(url, init, tries) {
63
+ let wait = retryDelay, last = null;
64
+ tries = tries || 4;
65
+ for (let i = 0; i < tries; i++) {
66
+ checkAbort();
67
+ try {
68
+ const r = await rawFetch(url, init);
69
+ if (r.status === 429 || r.status >= 500) {
70
+ last = new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
71
+ await sleep(wait); wait *= 2; continue;
72
+ }
73
+ if (!r.ok) throw new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
74
+ return await r.json();
75
+ } catch (e) {
76
+ if (isCancel(e) || (e.code === "HTTP" && e.data.status >= 400 && e.data.status < 500)) throw e;
77
+ last = e; await sleep(wait); wait *= 2;
78
+ }
79
+ }
80
+ if (last && last.code) throw last;
81
+ throw new ScanError("NETWORK", (last && last.message) || "no response", { cause: last && last.message });
82
+ }
83
+
84
+ // Fetch that never throws (except on cancel): used for optional extras such as RugCheck,
85
+ // which must never block the main result if they fail, time out or are down
86
+ async function softJson(url, timeoutMs) {
87
+ checkAbort();
88
+ const ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
89
+ let onAbort = null;
90
+ if (ctrl && signal) { onAbort = function () { ctrl.abort(); }; signal.addEventListener("abort", onAbort); }
91
+ const to = ctrl ? setTimeout(function () { ctrl.abort(); }, timeoutMs || 8000) : null;
92
+ try {
93
+ const r = await fetchImpl(url, ctrl ? { signal: ctrl.signal } : undefined);
94
+ if (!r.ok) return null;
95
+ return await r.json();
96
+ } catch (e) {
97
+ return null;
98
+ } finally {
99
+ if (to) clearTimeout(to);
100
+ if (onAbort) signal.removeEventListener("abort", onAbort);
101
+ checkAbort();
102
+ }
103
+ }
104
+
105
+ async function rpcAt(url, method, params, tries) {
106
+ const j = await jfetch(url, {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: method, params: params })
110
+ }, tries);
111
+ if (j.error) throw new ScanError("RPC", j.error.message || "RPC error", { rpcCode: j.error.code });
112
+ return j.result;
113
+ }
114
+
115
+ async function vibeSlot() {
116
+ const now = Date.now(), at = Math.max(now, vibeNext);
117
+ vibeNext = at + vibeInterval;
118
+ if (at > now) await sleep(at - now);
119
+ checkAbort();
120
+ }
121
+
122
+ async function rpc(method, params) {
123
+ if (ownRpc()) return rpcAt(rpcUrl, method, params);
124
+ // PublicNode accepts at most 10 accounts per getMultipleAccounts: split and merge the result
125
+ if (method === "getMultipleAccounts" && params[0].length > 10) {
126
+ const parts = [];
127
+ for (let i = 0; i < params[0].length; i += 10) parts.push(rpc(method, [params[0].slice(i, i + 10)].concat(params.slice(1))));
128
+ const rs = await Promise.all(parts);
129
+ return { context: rs[0].context, value: [].concat.apply([], rs.map(function (r) { return r.value; })) };
130
+ }
131
+ if (!blocked[method]) {
132
+ try {
133
+ const res = await rpcAt(publicRpc, method, params);
134
+ // PublicNode only keeps a few days of history: a list that ends before filling the page, or a
135
+ // transaction it cannot find, may just be old. Vibe Station keeps it all, so it gets the last word
136
+ if (method === "getSignaturesForAddress" && Array.isArray(res) && res.length < ((params[1] && params[1].limit) || 1000)) {
137
+ await vibeSlot();
138
+ const full = await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
139
+ return Array.isArray(full) && full.length > res.length ? full : res;
140
+ }
141
+ if (method === "getTransaction" && res === null) {
142
+ await vibeSlot();
143
+ return await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
144
+ }
145
+ return res;
146
+ }
147
+ catch (e) {
148
+ if (isCancel(e) || !(e.code === "HTTP" && (e.data.status === 401 || e.data.status === 403))) throw e;
149
+ blocked[method] = 1;
150
+ }
151
+ }
152
+ await vibeSlot();
153
+ return rpcAt(vibeRpc, method, params, 6); // more retries: its limit sometimes trips even when the queue is respected
154
+ }
155
+
156
+ return { rpc: rpc, jfetch: jfetch, softJson: softJson, ownRpc: ownRpc, heliusKey: heliusKey, checkAbort: checkAbort, rpcUrl: rpcUrl, retryDelayMs: opts.retryDelayMs };
157
+ }
package/src/constants.js CHANGED
@@ -1,93 +1,183 @@
1
- // api.mainnet-beta.solana.com answers 403 to anything coming from a browser. PublicNode serves almost
2
- // everything (accounts, signatures, transactions) quickly, but blocks token-by-owner and largest-holder
3
- // queries. Those go to Solana Vibe Station, which accepts them without a key but only at ~1 per second,
4
- // so they go through a queue (see client.js). With your own RPC URL everything goes to that URL.
5
- export const PUBLIC_RPC = "https://solana-rpc.publicnode.com";
6
- export const VIBE_RPC = "https://public.rpc.solanavibestation.com";
7
- export const DEX = "https://api.dexscreener.com/tokens/v1/solana/";
8
- // SolFlag's wallet base (see base.js)
9
- export const WALLET_BASE_URL = "https://solflag.com/datos/base.json";
10
- export const DEX_SEARCH = "https://api.dexscreener.com/latest/dex/search?q=";
11
- export const RUGCHECK = "https://api.rugcheck.xyz/v1/tokens/";
12
- export const GECKO = "https://api.geckoterminal.com/api/v2/networks/solana/";
13
- export const HELIUS_API = "https://api.helius.xyz/v0/addresses/";
14
- export const COINGECKO_SOL = "https://api.coingecko.com/api/v3/coins/solana/market_chart?vs_currency=usd&days=365";
15
-
16
- export const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
17
- export const TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
18
- export const SYSTEM_PROGRAM = "11111111111111111111111111111111";
19
- export const PUMP_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
20
- // dexId of a token still on Pump.fun's bonding curve: DexScreener says "pumpfun", GeckoTerminal "pump-fun"
21
- export const PUMP_CURVE_DEXES = ["pumpfun", "pump-fun"];
22
-
23
- export const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
24
- export const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
25
- export const WSOL = "So11111111111111111111111111111111111111112";
26
-
27
- export const KNOWN = {
28
- [WSOL]: "SOL (wrapped)",
29
- [USDC]: "USDC",
30
- [USDT]: "USDT"
31
- };
32
-
33
- // Symbols that scams usually copy, with their official address (checked on DexScreener)
34
- export const FAMOUS = {
35
- USDC, USDT, SOL: WSOL, WSOL,
36
- JUP: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", BONK: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
37
- WIF: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", PYTH: "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",
38
- JTO: "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", RAY: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
39
- PYUSD: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", MSOL: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
40
- JITOSOL: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", TRUMP: "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
41
- PUMP: "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", JLP: "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",
42
- POPCAT: "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", HNT: "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",
43
- RENDER: "rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", ORCA: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
44
- DRIFT: "DriFtupJYLTosbwoN8koMbEYSx54aFAVLddWsbksjwg7",
45
- // Bitcoin and Ether only exist on Solana bridged: a Pump.fun token called "BTC" is never the real one
46
- BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", WBTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",
47
- ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", WETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"
48
- };
49
-
50
- // Real companies with no token of their own on Solana: a memecoin called "Spotify" is not Spotify. Their
51
- // genuine tokenized shares come from an issuer (xStocks, Backpack...) and are typed TOKENIZED, so they
52
- // are never matched. Names only, lower case: stock tickers clash with real crypto symbols (META, COIN...)
53
- export const BRANDS = {
54
- spotify: "Spotify", shopify: "Shopify", tesla: "Tesla", apple: "Apple", nvidia: "Nvidia", amazon: "Amazon",
55
- google: "Google", alphabet: "Alphabet", microsoft: "Microsoft", facebook: "Facebook", instagram: "Instagram",
56
- whatsapp: "WhatsApp", netflix: "Netflix", coinbase: "Coinbase", robinhood: "Robinhood", gamestop: "GameStop",
57
- microstrategy: "MicroStrategy", paypal: "PayPal", visa: "Visa", mastercard: "Mastercard", nike: "Nike",
58
- disney: "Disney", starbucks: "Starbucks", mcdonalds: "McDonald's", walmart: "Walmart", uber: "Uber",
59
- airbnb: "Airbnb", tiktok: "TikTok", youtube: "YouTube", openai: "OpenAI", chatgpt: "ChatGPT", blackrock: "BlackRock",
60
- jpmorgan: "JPMorgan", samsung: "Samsung", sony: "Sony", nintendo: "Nintendo", playstation: "PlayStation",
61
- ferrari: "Ferrari", lamborghini: "Lamborghini", "red bull": "Red Bull", "coca cola": "Coca-Cola", pepsi: "Pepsi",
62
- adidas: "Adidas", gucci: "Gucci", louis_vuitton: "Louis Vuitton", rolex: "Rolex", spacex: "SpaceX", neuralink: "Neuralink"
63
- };
64
-
65
- // Known issuers of tokenized assets: every key that controls their tokens (mint, freeze, permanent delegate,
66
- // hook, pause, scaling). A token is theirs only if ALL its control keys are in the list, so copying one key
67
- // (anyone can hand the mint authority to someone else's wallet) is not enough. Their control is the point of
68
- // the product: it is shown as dependency on the issuer, not as a creator who can rug
69
- export const ISSUERS = [
70
- { name: "xStocks (Backed)", keys: [
71
- "7pt9tkctJPK7PPNQJ77GKg8ZffSF6QxoMiCFYHxrtaCj", // mint
72
- "JDq14BWvqCRFNu1krb12bcRpbGtJZ1FLEakMw6FdxJNs", // freeze and pause (program)
73
- "5aMNNLQJwAEeoemTEMkv5NVjqKwvvefRYCQ5Z67HFvEq", // permanent delegate, transfer hook, confidential transfers
74
- "S7vYFFWH6BjJyEsdrPQpqpYTqLTrPRK6KW3VwsJuRaS" // share-split multiplier (scaled UI amount)
75
- ] }
76
- ];
77
-
78
- // Token-2022 extensions that add risk, with the points they add to the no-market spam score
79
- export const EXT_POINTS = {
80
- permanentDelegate: 25,
81
- transferHook: 15,
82
- transferFeeConfig: 10,
83
- nonTransferable: 10,
84
- defaultAccountState: 10
85
- };
86
-
87
- // Risk levels, from worst to best (also the default sort order of results)
88
- export const LEVELS = ["HIGH", "MEDIUM", "LOW", "SPAM", "NO_DATA", "NO_MARKET"];
89
- export const TYPES = ["STABLE", "TOKENIZED", "BRIDGED", "CREATOR", "OPEN"];
90
- export const LIGHTS = ["control", "concentration", "market", "dependency"];
91
- export const RANKS = { ok: 0, na: 0, warn: 1, bad: 2 };
92
-
93
- export const ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
1
+ // api.mainnet-beta.solana.com answers 403 to anything coming from a browser. PublicNode serves almost
2
+ // everything (accounts, signatures, transactions) quickly, but blocks token-by-owner and largest-holder
3
+ // queries. Those go to Solana Vibe Station, which accepts them without a key but only at ~1 per second,
4
+ // so they go through a queue (see client.js). With your own RPC URL everything goes to that URL.
5
+ export const PUBLIC_RPC = "https://solana-rpc.publicnode.com";
6
+ export const VIBE_RPC = "https://public.rpc.solanavibestation.com";
7
+ export const DEX = "https://api.dexscreener.com/tokens/v1/solana/";
8
+ // SolFlag's wallet base (see base.js)
9
+ export const WALLET_BASE_URL = "https://solflag.com/datos/base.json";
10
+ export const DEX_SEARCH = "https://api.dexscreener.com/latest/dex/search?q=";
11
+ export const RUGCHECK = "https://api.rugcheck.xyz/v1/tokens/";
12
+ export const GECKO = "https://api.geckoterminal.com/api/v2/networks/solana/";
13
+ export const HELIUS_API = "https://api.helius.xyz/v0/addresses/";
14
+ export const COINGECKO_SOL = "https://api.coingecko.com/api/v3/coins/solana/market_chart?vs_currency=usd&days=365";
15
+
16
+ export const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
17
+ export const TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
18
+ export const SYSTEM_PROGRAM = "11111111111111111111111111111111";
19
+ export const PUMP_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
20
+ // dexId of a token still on Pump.fun's bonding curve: DexScreener says "pumpfun", GeckoTerminal "pump-fun"
21
+ export const PUMP_CURVE_DEXES = ["pumpfun", "pump-fun"];
22
+
23
+ export const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
24
+ export const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
25
+ export const WSOL = "So11111111111111111111111111111111111111112";
26
+
27
+ export const KNOWN = {
28
+ [WSOL]: "SOL (wrapped)",
29
+ [USDC]: "USDC",
30
+ [USDT]: "USDT"
31
+ };
32
+
33
+ // Symbols that scams usually copy, with their official address (checked on DexScreener)
34
+ export const FAMOUS = {
35
+ USDC, USDT, SOL: WSOL, WSOL,
36
+ JUP: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", BONK: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
37
+ WIF: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", PYTH: "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",
38
+ JTO: "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", RAY: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
39
+ PYUSD: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", MSOL: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
40
+ JITOSOL: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", TRUMP: "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
41
+ PUMP: "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", JLP: "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",
42
+ POPCAT: "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", HNT: "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",
43
+ RENDER: "rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", ORCA: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
44
+ DRIFT: "DriFtupJYLTosbwoN8koMbEYSx54aFAVLddWsbksjwg7",
45
+ // Bitcoin and Ether only exist on Solana bridged: a Pump.fun token called "BTC" is never the real one
46
+ BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", WBTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",
47
+ ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", WETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"
48
+ };
49
+
50
+ // Real companies with no token of their own on Solana: a memecoin called "Spotify" is not Spotify. Clubs
51
+ // with an official fan token on Solana (Barcelona, PSG, Juventus...) and Western Union (USDPT) are left out. Their
52
+ // genuine tokenized shares come from an issuer (xStocks, Backpack...) and are typed TOKENIZED, so they
53
+ // are never matched. Names only: stock tickers clash with real crypto symbols (META, COIN...). A name that
54
+ // is also an everyday word or a first name (Shell, Target, Puma, Mango, Oracle, Corona, Hermes...) is left
55
+ // out, since a token called just that would be marked HIGH. brandKey() normalises both sides
56
+ export function brandKey(s) {
57
+ return String(s || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()
58
+ .replace(/[$'’.\-_]/g, " ").replace(/\s+/g, " ").trim();
59
+ }
60
+ const BRAND_NAMES = [
61
+ // tech, internet and AI
62
+ "Apple", "Google", "Alphabet", "Microsoft", "Amazon", "Nvidia", "Facebook", "Instagram", "WhatsApp", "Netflix",
63
+ "Spotify", "Shopify", "TikTok", "YouTube", "OpenAI", "ChatGPT", "Anthropic", "DeepSeek", "Midjourney", "Samsung",
64
+ "Sony", "Nintendo", "PlayStation", "Xbox", "Intel", "Qualcomm", "Broadcom", "Salesforce", "Adobe", "IBM", "Cisco",
65
+ "Dell", "Lenovo", "Huawei", "Xiaomi", "OnePlus", "Motorola", "Nokia", "Snapchat", "Pinterest", "Reddit", "LinkedIn",
66
+ "Twitter", "Telegram", "Discord", "Twitch", "Dropbox", "Airbnb", "Uber", "Lyft", "DoorDash", "Expedia", "eBay",
67
+ "Alibaba", "AliExpress", "Temu", "Shein", "Tencent", "Baidu", "ByteDance", "Palantir", "SpaceX", "Starlink",
68
+ "Neuralink", "Roblox", "Fortnite", "Epic Games", "Minecraft", "Ubisoft", "Activision", "Rockstar Games",
69
+ "Electronic Arts", "Panasonic", "Philips", "Siemens", "Bosch", "Nikon", "GoPro", "Bose", "Dyson", "Garmin",
70
+ "Fitbit", "Peloton", "Logitech", "Razer", "Asus", "Xerox", "Vodafone", "Verizon", "AT&T", "T-Mobile", "Comcast",
71
+ "Movistar", "Telefonica", "Wikipedia", "Duolingo",
72
+ // finance and payments
73
+ "Visa", "Mastercard", "PayPal", "American Express", "JPMorgan", "Goldman Sachs", "Morgan Stanley", "BlackRock",
74
+ "Vanguard", "Fidelity", "Citibank", "Wells Fargo", "Bank of America", "HSBC", "Barclays", "Santander", "BBVA",
75
+ "CaixaBank", "Deutsche Bank", "Revolut", "Stripe", "Klarna", "Robinhood", "Charles Schwab", "Nasdaq", "NYSE",
76
+ "Berkshire Hathaway", "MoneyGram", "Venmo", "Cash App", "Zelle", "Monzo", "N26", "Nubank",
77
+ "Mapfre", "Bankinter",
78
+ // crypto companies with no token on Solana
79
+ "Coinbase", "Binance", "Bybit", "OKX", "KuCoin", "Bitget", "Upbit", "Bithumb", "Bitpanda", "eToro",
80
+ "MicroStrategy", "Grayscale", "MetaMask", "Trezor", "Solflare",
81
+ // fashion, retail and consumer goods
82
+ "Nike", "Adidas", "Reebok", "New Balance", "Under Armour", "Zara", "Inditex", "H&M", "Uniqlo", "Levi's", "Lacoste",
83
+ "Ralph Lauren", "Tommy Hilfiger", "Calvin Klein", "Gucci", "Louis Vuitton", "Chanel", "Prada", "Dior", "Versace",
84
+ "Balenciaga", "Burberry", "Fendi", "Givenchy", "Yves Saint Laurent", "Armani", "Dolce & Gabbana", "Rolex",
85
+ "Cartier", "Patek Philippe", "Audemars Piguet", "Swatch", "Ray-Ban", "Sephora", "L'Oreal", "Nivea", "Gillette",
86
+ "Colgate", "Pampers", "IKEA", "Lego", "Mattel", "Barbie", "Hot Wheels", "Hasbro", "Pokemon", "Hello Kitty",
87
+ "Sanrio", "Walmart", "Costco", "Carrefour", "Mercadona", "El Corte Ingles", "Lidl", "Aldi", "Tesco",
88
+ "Home Depot", "Best Buy", "7-Eleven", "Wallapop", "Glovo", "Cabify",
89
+ // food and drink
90
+ "Coca-Cola", "Pepsi", "Red Bull", "Monster Energy", "Starbucks", "McDonald's", "Burger King", "KFC", "Domino's",
91
+ "Pizza Hut", "Taco Bell", "Wendy's", "Chipotle", "Dunkin", "Krispy Kreme", "Nestle", "Nutella", "Ferrero",
92
+ "Doritos", "Pringles", "Cheetos", "Heinz", "Kellogg's", "Heineken", "Budweiser", "Guinness",
93
+ "Jack Daniel's", "Johnnie Walker", "Hennessy", "Moet", "Dom Perignon", "Smirnoff", "Bacardi", "Gatorade", "Fanta",
94
+ "Nespresso", "Haribo", "Snickers", "M&M's", "Hershey", "Cadbury", "Lindt",
95
+ // cars and motorbikes
96
+ "Tesla", "Ferrari", "Lamborghini", "Porsche", "Bugatti", "McLaren", "Bentley", "Rolls-Royce", "Aston Martin",
97
+ "Maserati", "BMW", "Mercedes-Benz", "Audi", "Volkswagen", "Toyota", "Honda", "Nissan", "Hyundai", "Chevrolet",
98
+ "Cadillac", "Volvo", "Peugeot", "Renault", "Harley-Davidson", "Ducati", "Yamaha", "Kawasaki", "Suzuki", "Rivian",
99
+ "Polestar",
100
+ // media, entertainment and sport
101
+ "Disney", "Pixar", "Marvel", "Warner Bros", "HBO", "Paramount", "Universal Studios", "DreamWorks", "Hulu",
102
+ "Fox News", "CNN", "ESPN", "National Geographic", "Playboy", "Nickelodeon", "Cartoon Network",
103
+ "Star Wars", "NBA", "NFL", "FIFA", "UEFA", "WWE", "Formula 1", "Real Madrid", "Manchester United",
104
+ "Bayern Munich",
105
+ // travel, transport, energy and health
106
+ "Emirates", "Qatar Airways", "Ryanair", "Lufthansa", "Boeing", "Airbus", "Marriott", "FedEx", "DHL",
107
+ "ExxonMobil", "Chevron", "Aramco", "Repsol", "Iberdrola", "Pfizer", "Moderna", "Johnson & Johnson",
108
+ "Procter & Gamble", "Unilever"
109
+ ];
110
+ // Products that companies (or their partners) really put on Solana carry the name too: exchange staked SOL,
111
+ // fund shares, wrapped BTC. They trade for weeks with real liquidity, while copies are new and thin, so a
112
+ // market at least this old AND this deep is not checked for brand names
113
+ export const BRAND_MATURE_DAYS = 30;
114
+ export const BRAND_MATURE_LIQ = 100000;
115
+ // COPYCAT: the token taken as the original must be a real market. Farms relaunch the same name and ticker
116
+ // many times a day, so the "oldest" is often another dead copy from an hour ago (registry 29/10: 209 of 320
117
+ // new tokens flagged). Below this liquidity the older token is not something worth copying
118
+ export const COPY_MIN_LIQ = 10000;
119
+ export const BRANDS = Object.fromEntries(BRAND_NAMES.map(function (n) { return [brandKey(n), n]; }));
120
+
121
+ // Official tokens put on Solana by exchanges and by Socios.com, by mint address (a copy always has another
122
+ // address). They are bought and sold mostly on their exchange, so Solana pools are tiny and the issuer's
123
+ // custody wallet holds most of the supply: read as a stranger's token that gave HIGH (bnSOL, the Barcelona
124
+ // fan token). They are shown as dependency on the issuer, like ISSUERS below
125
+ export const OFFICIAL_TOKENS = {
126
+ BNso1VUJnh4zcfpZa6986Ea66P6TCp59hvtNJ8b1X85: "Binance", // Binance Staked SOL
127
+ Bybit2vBJGhPF52GBdNaQfUJ6ZpThSgHBobjWZpLPb4B: "Bybit", // Bybit Staked SOL
128
+ bgSoLfRx1wRPehwC9TyG568AGjnf1sQG1MYa8s3FbfY: "Bitget", // Bitget Staked SOL
129
+ "7cBuurYDdaqxnem7KyMTci6SWhjJKroZ6NUjqH2ewEPB": "MEXC", // MEXC Staked SOL
130
+ CDCSoLckzozyktpAp9FWT3w92KFJVEUxAU7cNu2Jn3aX: "Crypto.com", // Crypto.com Staked SOL
131
+ FDLkS4zH5rGmD7q7qGfzhmzPgQJ88jcX1JFT5nfdNTjg: "Socios.com (Chiliz)", // Olympique de Marseille Fan Token
132
+ BjXGwMnWWUkS9JrXEkkamszu4oX8oqo3kNqFr3ifNk1F: "Socios.com (Chiliz)", // Napoli Fan Token
133
+ BX8VbHBf8DUHeGJjJsSW2HCviGyQSF5SVCEnN7vXUkwM: "Socios.com (Chiliz)", // Argentine Football Association Fan Token
134
+ AiYi7HyVVXnDSuj8BoPirDsuuWgddeKkD5MQN35VpwNz: "Socios.com (Chiliz)", // Portugal National Team Fan Token
135
+ HD9i9C2Pw6hWXm354i3SWkYG8ZtfikUJn2bNPzwQZcEK: "Socios.com (Chiliz)", // SL Benfica Fan Token
136
+ cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij: "Coinbase", // Coinbase Wrapped BTC
137
+ gtBTCGWvSRYYoZpU9UZj6i3eUGUpgksXzzsbHk2K9So: "Gate", // Gate Wrapped BTC
138
+ CtzPWv73Sn1dMGVU3ZtLv9yWSyUAanBni19YWDaznnkn: "OKX", // OKX Wrapped BTC
139
+ BtjXf3LA7Z3jxAcQkKUqKYpLFS5B7GuB7pfjzeas1tdB: "Socios.com (Chiliz)", // OG Fan Token
140
+ "6VdpC1hcx7byFtJ5M6Xhspa6Uup5rZHruzXvuJhXAyXm": "Socios.com (Chiliz)", // Atletico Madrid Fan Token
141
+ EDzhmkPa5gToYGowjH66xycCRgPmfWNzfqpXdCzsYb8M: "Socios.com (Chiliz)", // Galatasaray Fan Token
142
+ "82DNsTK61ZrgCHP6pfP32Eubcsp9h38d64E6X9ETEBBe": "Socios.com (Chiliz)", // FC Barcelona Fan Token
143
+ "5eyib4qghYGHNh7VvxSFGYLFJSanjq9hug9fR52kksnm": "Socios.com (Chiliz)", // Paris Saint-Germain Fan Token
144
+ "3cVuTnMj8FeqK82PDgK4JzKnLP7LM6GauqTyqnxtcSk1": "Socios.com (Chiliz)", // AS Roma Fan Token
145
+ HE6bLNs16qgQo2jVSMXDSCnxQahjdVrww7qQPrHrZtEw: "Socios.com (Chiliz)", // Juventus Fan Token
146
+ "8WbNQtY7QmXMVKJFTSqFudierVZZtbuoyeepZEqJ1B2w": "Socios.com (Chiliz)", // Manchester City Fan Token
147
+ H5qGPniSX2uCNtAnxr7RpdfFAZcGkr6dknjgpa1AKHe1: "Socios.com (Chiliz)", // AC Milan Fan Token
148
+ Dst93spXQEXxFzwYbFrQRnFYBYpw7AzB2QyALEeP4NGQ: "Socios.com (Chiliz)", // Arsenal Fan Token
149
+ HagS3VmAuhx5QJnHcuMAJF4wwVDE58CiX5SFZCYFrVe: "Socios.com (Chiliz)", // Inter Milan Fan Token
150
+ AMnbxhGifp3gm2YsNwmfcF2ZCLsZN6iP3LdzBPZgGpzW: "Socios.com (Chiliz)", // Trabzonspor Fan Token
151
+ GDNoEjfSAUAvManoEaT3vStGcvwKkcUcSTvUwfvwhqXf: "Socios.com (Chiliz)", // Tottenham Hotspur FC Fan Token
152
+ FG44Jffowai8hqQifkwtNLbADkH3EjG5txMc54yaJ4cN: "Socios.com (Chiliz)", // UFC Fan Token
153
+ };
154
+
155
+ // Known issuers of tokenized assets: every key that controls their tokens (mint, freeze, permanent delegate,
156
+ // hook, pause, scaling). A token is theirs only if ALL its control keys are in the list, so copying one key
157
+ // (anyone can hand the mint authority to someone else's wallet) is not enough. Their control is the point of
158
+ // the product: it is shown as dependency on the issuer, not as a creator who can rug
159
+ export const ISSUERS = [
160
+ { name: "xStocks (Backed)", keys: [
161
+ "7pt9tkctJPK7PPNQJ77GKg8ZffSF6QxoMiCFYHxrtaCj", // mint
162
+ "JDq14BWvqCRFNu1krb12bcRpbGtJZ1FLEakMw6FdxJNs", // freeze and pause (program)
163
+ "5aMNNLQJwAEeoemTEMkv5NVjqKwvvefRYCQ5Z67HFvEq", // permanent delegate, transfer hook, confidential transfers
164
+ "S7vYFFWH6BjJyEsdrPQpqpYTqLTrPRK6KW3VwsJuRaS" // share-split multiplier (scaled UI amount)
165
+ ] }
166
+ ];
167
+
168
+ // Token-2022 extensions that add risk, with the points they add to the no-market spam score
169
+ export const EXT_POINTS = {
170
+ permanentDelegate: 25,
171
+ transferHook: 15,
172
+ transferFeeConfig: 10,
173
+ nonTransferable: 10,
174
+ defaultAccountState: 10
175
+ };
176
+
177
+ // Risk levels, from worst to best (also the default sort order of results)
178
+ export const LEVELS = ["HIGH", "MEDIUM", "LOW", "SPAM", "NO_DATA", "NO_MARKET"];
179
+ export const TYPES = ["STABLE", "TOKENIZED", "BRIDGED", "CREATOR", "OPEN"];
180
+ export const LIGHTS = ["control", "concentration", "market", "dependency"];
181
+ export const RANKS = { ok: 0, na: 0, warn: 1, bad: 2 };
182
+
183
+ export const ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;