solana-risk-engine 0.4.3 → 0.4.5

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/constants.js CHANGED
@@ -1,78 +1,179 @@
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
- export const DEX_SEARCH = "https://api.dexscreener.com/latest/dex/search?q=";
9
- export const RUGCHECK = "https://api.rugcheck.xyz/v1/tokens/";
10
- export const GECKO = "https://api.geckoterminal.com/api/v2/networks/solana/";
11
- export const HELIUS_API = "https://api.helius.xyz/v0/addresses/";
12
- export const COINGECKO_SOL = "https://api.coingecko.com/api/v3/coins/solana/market_chart?vs_currency=usd&days=365";
13
-
14
- export const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
15
- export const TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
16
- export const SYSTEM_PROGRAM = "11111111111111111111111111111111";
17
- export const PUMP_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
18
- // dexId of a token still on Pump.fun's bonding curve: DexScreener says "pumpfun", GeckoTerminal "pump-fun"
19
- export const PUMP_CURVE_DEXES = ["pumpfun", "pump-fun"];
20
-
21
- export const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
22
- export const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
23
- export const WSOL = "So11111111111111111111111111111111111111112";
24
-
25
- export const KNOWN = {
26
- [WSOL]: "SOL (wrapped)",
27
- [USDC]: "USDC",
28
- [USDT]: "USDT"
29
- };
30
-
31
- // Symbols that scams usually copy, with their official address (checked on DexScreener)
32
- export const FAMOUS = {
33
- USDC, USDT, SOL: WSOL, WSOL,
34
- JUP: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", BONK: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
35
- WIF: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", PYTH: "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",
36
- JTO: "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", RAY: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
37
- PYUSD: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", MSOL: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
38
- JITOSOL: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", TRUMP: "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
39
- PUMP: "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", JLP: "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",
40
- POPCAT: "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", HNT: "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",
41
- RENDER: "rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", ORCA: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
42
- DRIFT: "DriFtupJYLTosbwoN8koMbEYSx54aFAVLddWsbksjwg7",
43
- // Bitcoin and Ether only exist on Solana bridged: a Pump.fun token called "BTC" is never the real one
44
- BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", WBTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",
45
- ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", WETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"
46
- };
47
-
48
- // Real companies with no token of their own on Solana: a memecoin called "Spotify" is not Spotify. Their
49
- // genuine tokenized shares come from an issuer (xStocks, Backpack...) and are typed TOKENIZED, so they
50
- // are never matched. Names only, lower case: stock tickers clash with real crypto symbols (META, COIN...)
51
- export const BRANDS = {
52
- spotify: "Spotify", shopify: "Shopify", tesla: "Tesla", apple: "Apple", nvidia: "Nvidia", amazon: "Amazon",
53
- google: "Google", alphabet: "Alphabet", microsoft: "Microsoft", facebook: "Facebook", instagram: "Instagram",
54
- whatsapp: "WhatsApp", netflix: "Netflix", coinbase: "Coinbase", robinhood: "Robinhood", gamestop: "GameStop",
55
- microstrategy: "MicroStrategy", paypal: "PayPal", visa: "Visa", mastercard: "Mastercard", nike: "Nike",
56
- disney: "Disney", starbucks: "Starbucks", mcdonalds: "McDonald's", walmart: "Walmart", uber: "Uber",
57
- airbnb: "Airbnb", tiktok: "TikTok", youtube: "YouTube", openai: "OpenAI", chatgpt: "ChatGPT", blackrock: "BlackRock",
58
- jpmorgan: "JPMorgan", samsung: "Samsung", sony: "Sony", nintendo: "Nintendo", playstation: "PlayStation",
59
- ferrari: "Ferrari", lamborghini: "Lamborghini", "red bull": "Red Bull", "coca cola": "Coca-Cola", pepsi: "Pepsi",
60
- adidas: "Adidas", gucci: "Gucci", louis_vuitton: "Louis Vuitton", rolex: "Rolex", spacex: "SpaceX", neuralink: "Neuralink"
61
- };
62
-
63
- // Token-2022 extensions that add risk, with the points they add to the no-market spam score
64
- export const EXT_POINTS = {
65
- permanentDelegate: 25,
66
- transferHook: 15,
67
- transferFeeConfig: 10,
68
- nonTransferable: 10,
69
- defaultAccountState: 10
70
- };
71
-
72
- // Risk levels, from worst to best (also the default sort order of results)
73
- export const LEVELS = ["HIGH", "MEDIUM", "LOW", "SPAM", "NO_DATA", "NO_MARKET"];
74
- export const TYPES = ["STABLE", "TOKENIZED", "BRIDGED", "CREATOR", "OPEN"];
75
- export const LIGHTS = ["control", "concentration", "market", "dependency"];
76
- export const RANKS = { ok: 0, na: 0, warn: 1, bad: 2 };
77
-
78
- 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
+ export const BRANDS = Object.fromEntries(BRAND_NAMES.map(function (n) { return [brandKey(n), n]; }));
116
+
117
+ // Official tokens put on Solana by exchanges and by Socios.com, by mint address (a copy always has another
118
+ // address). They are bought and sold mostly on their exchange, so Solana pools are tiny and the issuer's
119
+ // custody wallet holds most of the supply: read as a stranger's token that gave HIGH (bnSOL, the Barcelona
120
+ // fan token). They are shown as dependency on the issuer, like ISSUERS below
121
+ export const OFFICIAL_TOKENS = {
122
+ BNso1VUJnh4zcfpZa6986Ea66P6TCp59hvtNJ8b1X85: "Binance", // Binance Staked SOL
123
+ Bybit2vBJGhPF52GBdNaQfUJ6ZpThSgHBobjWZpLPb4B: "Bybit", // Bybit Staked SOL
124
+ bgSoLfRx1wRPehwC9TyG568AGjnf1sQG1MYa8s3FbfY: "Bitget", // Bitget Staked SOL
125
+ "7cBuurYDdaqxnem7KyMTci6SWhjJKroZ6NUjqH2ewEPB": "MEXC", // MEXC Staked SOL
126
+ CDCSoLckzozyktpAp9FWT3w92KFJVEUxAU7cNu2Jn3aX: "Crypto.com", // Crypto.com Staked SOL
127
+ FDLkS4zH5rGmD7q7qGfzhmzPgQJ88jcX1JFT5nfdNTjg: "Socios.com (Chiliz)", // Olympique de Marseille Fan Token
128
+ BjXGwMnWWUkS9JrXEkkamszu4oX8oqo3kNqFr3ifNk1F: "Socios.com (Chiliz)", // Napoli Fan Token
129
+ BX8VbHBf8DUHeGJjJsSW2HCviGyQSF5SVCEnN7vXUkwM: "Socios.com (Chiliz)", // Argentine Football Association Fan Token
130
+ AiYi7HyVVXnDSuj8BoPirDsuuWgddeKkD5MQN35VpwNz: "Socios.com (Chiliz)", // Portugal National Team Fan Token
131
+ HD9i9C2Pw6hWXm354i3SWkYG8ZtfikUJn2bNPzwQZcEK: "Socios.com (Chiliz)", // SL Benfica Fan Token
132
+ cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij: "Coinbase", // Coinbase Wrapped BTC
133
+ gtBTCGWvSRYYoZpU9UZj6i3eUGUpgksXzzsbHk2K9So: "Gate", // Gate Wrapped BTC
134
+ CtzPWv73Sn1dMGVU3ZtLv9yWSyUAanBni19YWDaznnkn: "OKX", // OKX Wrapped BTC
135
+ BtjXf3LA7Z3jxAcQkKUqKYpLFS5B7GuB7pfjzeas1tdB: "Socios.com (Chiliz)", // OG Fan Token
136
+ "6VdpC1hcx7byFtJ5M6Xhspa6Uup5rZHruzXvuJhXAyXm": "Socios.com (Chiliz)", // Atletico Madrid Fan Token
137
+ EDzhmkPa5gToYGowjH66xycCRgPmfWNzfqpXdCzsYb8M: "Socios.com (Chiliz)", // Galatasaray Fan Token
138
+ "82DNsTK61ZrgCHP6pfP32Eubcsp9h38d64E6X9ETEBBe": "Socios.com (Chiliz)", // FC Barcelona Fan Token
139
+ "5eyib4qghYGHNh7VvxSFGYLFJSanjq9hug9fR52kksnm": "Socios.com (Chiliz)", // Paris Saint-Germain Fan Token
140
+ "3cVuTnMj8FeqK82PDgK4JzKnLP7LM6GauqTyqnxtcSk1": "Socios.com (Chiliz)", // AS Roma Fan Token
141
+ HE6bLNs16qgQo2jVSMXDSCnxQahjdVrww7qQPrHrZtEw: "Socios.com (Chiliz)", // Juventus Fan Token
142
+ "8WbNQtY7QmXMVKJFTSqFudierVZZtbuoyeepZEqJ1B2w": "Socios.com (Chiliz)", // Manchester City Fan Token
143
+ H5qGPniSX2uCNtAnxr7RpdfFAZcGkr6dknjgpa1AKHe1: "Socios.com (Chiliz)", // AC Milan Fan Token
144
+ Dst93spXQEXxFzwYbFrQRnFYBYpw7AzB2QyALEeP4NGQ: "Socios.com (Chiliz)", // Arsenal Fan Token
145
+ HagS3VmAuhx5QJnHcuMAJF4wwVDE58CiX5SFZCYFrVe: "Socios.com (Chiliz)", // Inter Milan Fan Token
146
+ AMnbxhGifp3gm2YsNwmfcF2ZCLsZN6iP3LdzBPZgGpzW: "Socios.com (Chiliz)", // Trabzonspor Fan Token
147
+ GDNoEjfSAUAvManoEaT3vStGcvwKkcUcSTvUwfvwhqXf: "Socios.com (Chiliz)", // Tottenham Hotspur FC Fan Token
148
+ FG44Jffowai8hqQifkwtNLbADkH3EjG5txMc54yaJ4cN: "Socios.com (Chiliz)", // UFC Fan Token
149
+ };
150
+
151
+ // Known issuers of tokenized assets: every key that controls their tokens (mint, freeze, permanent delegate,
152
+ // hook, pause, scaling). A token is theirs only if ALL its control keys are in the list, so copying one key
153
+ // (anyone can hand the mint authority to someone else's wallet) is not enough. Their control is the point of
154
+ // the product: it is shown as dependency on the issuer, not as a creator who can rug
155
+ export const ISSUERS = [
156
+ { name: "xStocks (Backed)", keys: [
157
+ "7pt9tkctJPK7PPNQJ77GKg8ZffSF6QxoMiCFYHxrtaCj", // mint
158
+ "JDq14BWvqCRFNu1krb12bcRpbGtJZ1FLEakMw6FdxJNs", // freeze and pause (program)
159
+ "5aMNNLQJwAEeoemTEMkv5NVjqKwvvefRYCQ5Z67HFvEq", // permanent delegate, transfer hook, confidential transfers
160
+ "S7vYFFWH6BjJyEsdrPQpqpYTqLTrPRK6KW3VwsJuRaS" // share-split multiplier (scaled UI amount)
161
+ ] }
162
+ ];
163
+
164
+ // Token-2022 extensions that add risk, with the points they add to the no-market spam score
165
+ export const EXT_POINTS = {
166
+ permanentDelegate: 25,
167
+ transferHook: 15,
168
+ transferFeeConfig: 10,
169
+ nonTransferable: 10,
170
+ defaultAccountState: 10
171
+ };
172
+
173
+ // Risk levels, from worst to best (also the default sort order of results)
174
+ export const LEVELS = ["HIGH", "MEDIUM", "LOW", "SPAM", "NO_DATA", "NO_MARKET"];
175
+ export const TYPES = ["STABLE", "TOKENIZED", "BRIDGED", "CREATOR", "OPEN"];
176
+ export const LIGHTS = ["control", "concentration", "market", "dependency"];
177
+ export const RANKS = { ok: 0, na: 0, warn: 1, bad: 2 };
178
+
179
+ export const ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
package/src/holders.js CHANGED
@@ -4,6 +4,7 @@ import { pool, progress, short, uniq } from "./util.js";
4
4
  import { getMint, accountKinds } from "./tokens.js";
5
5
  import { getMarkets } from "./markets.js";
6
6
  import { rugcheckReport, rugcheckGraph, rugcheckSummary, netBalances } from "./rugcheck.js";
7
+ import { loadWalletBase, knownType } from "./base.js";
7
8
 
8
9
  // Largest accounts of a token, each traced to its owner, and whether each owner is a wallet or a program
9
10
  export async function holderBase(client, mint, onProgress) {
@@ -203,6 +204,9 @@ export async function creatorInfo(client, mint, onProgress) {
203
204
  * over 10% of the supply in a young token (bad) or 15% in a mature one (warn) is a HOLD_GROUP signal.
204
205
  *
205
206
  * @param [opts.creator=true] also look up the creator and insider networks on RugCheck
207
+ * @param [opts.base] SolFlag's wallet base: true to load it (solflag.com/datos/base.json), or the base
208
+ * itself. Known exchanges are taken as such without measuring them, and every row
209
+ * it knows gets known: "exchange" | "granja" | "repartidor" | "creador en serie" | "sniper"
206
210
  * @param [opts.createdAt] ms when its market started, if the caller knows it
207
211
  * @param [opts.now]
208
212
  * @returns {..., mode: "young"|"mature", ageDays, groups, signals}
@@ -215,6 +219,13 @@ export async function analyzeHolders(client, mint, opts) {
215
219
  const wal = b.uniq.filter(function (o) { return b.kind[o] === "wallet"; });
216
220
  const busy = await markBusy(client, b.kind, wal, function (c, n) { progress(onProgress, "bots", 0.65 + 0.15 * c / Math.max(1, n), { done: c, total: n }); });
217
221
  const hr = holderRows(b, busy), pair = b.pair;
222
+ const base = opts.base === true ? await loadWalletBase(client) : (opts.base || null);
223
+ hr.rows.forEach(function (r) {
224
+ const t = knownType(base, r.owner);
225
+ if (!t) return;
226
+ r.known = t;
227
+ if (t === "exchange" && r.kind === "wallet") r.kind = "active"; // quiet hours made it look like a person
228
+ });
218
229
  const now = opts.now || Date.now();
219
230
  let created = opts.createdAt || (pair && pair.pairCreatedAt) || null;
220
231
  if (!created) {
@@ -234,9 +245,9 @@ export async function analyzeHolders(client, mint, opts) {
234
245
  if (srcs.length) {
235
246
  // a source is an exchange when it moves more than EXCHANGE_PER_HOUR transactions an hour (the bot
236
247
  // test, 200 in 2 days, also caught distributors doing ~50 an hour and hid their groups)
237
- const rates = await sourceRates(client, srcs, function (c, n) { progress(onProgress, "exchangeSources", 0.88 + 0.02 * c / Math.max(1, n), { done: c, total: n }); });
238
- const hot = {};
239
- srcs.forEach(function (f) { if (rates[f] > EXCHANGE_PER_HOUR) hot[f] = true; });
248
+ const hot = {}, ask = srcs.filter(function (f) { if (knownType(base, f) === "exchange") { hot[f] = true; return false; } return true; });
249
+ const rates = ask.length ? await sourceRates(client, ask, function (c, n) { progress(onProgress, "exchangeSources", 0.88 + 0.02 * c / Math.max(1, n), { done: c, total: n }); }) : {};
250
+ ask.forEach(function (f) { if (rates[f] > EXCHANGE_PER_HOUR) hot[f] = true; });
240
251
  hotGroups = srcs.filter(function (f) { return hot[f]; }).length;
241
252
  if (hotGroups) clusters = clusterByFunder(funders, hot);
242
253
  }
@@ -255,6 +266,9 @@ export async function analyzeHolders(client, mint, opts) {
255
266
  mint: mint, sym: (pair && pair.baseToken && pair.baseToken.symbol) || short(mint),
256
267
  name: (pair && pair.baseToken && pair.baseToken.name) || "", price: hr.price, rows: hr.rows,
257
268
  mintAuth: !!b.info.mintAuth, freezeAuth: !!b.info.freezeAuth, clusters: clusters, hotGroups: hotGroups, creator: creator,
258
- mode: mode, ageDays: ageDays, groups: groups, signals: signals
269
+ mode: mode, ageDays: ageDays, groups: groups, signals: signals,
270
+ // sources of groups the base knows (a known distributor or farm behind a group is worth saying)
271
+ knownSources: base ? uniq(groups.map(function (g) { return g.funder; })).filter(function (f) { return knownType(base, f); })
272
+ .reduce(function (o, f) { o[f] = knownType(base, f); return o; }, {}) : {}
259
273
  };
260
274
  }
package/src/i18n/en.js CHANGED
@@ -35,6 +35,7 @@ export default {
35
35
  CTL_NON_TRANSFERABLE: () => "Cannot be transferred",
36
36
  CTL_DEFAULT_FROZEN: () => "New accounts can be created frozen",
37
37
  CTL_ONLY_PROGRAMS: () => "Only program-controlled authorities (see dependency)",
38
+ CTL_KNOWN_ISSUER: (d) => "Controlled by its issuer, " + d.issuer + " (see dependency)",
38
39
  CTL_NONE: () => "No active authorities: nobody can mint more or freeze",
39
40
  CTL_SERIAL_CREATOR: (d) => "The creator launched " + d.n + " other tokens in the last 24 h (per RugCheck): a serial launcher, who usually drops each token as soon as buyers arrive",
40
41
  BRAND_COPY: (d) => "Uses the name of " + d.brand + ", a real company with no token on Solana: it is not official, almost certainly an imitation to draw buyers",
@@ -64,6 +65,7 @@ export default {
64
65
  DEP_PROGRAM_AUTHORITY: () => "A program controls the mint or freeze (bridge or protocol): you have to trust its code",
65
66
  DEP_PERMANENT_DELEGATE: () => "The issuer can move or burn your tokens (permanent delegate): you trust the issuer",
66
67
  DEP_STABLE_ISSUER: () => "Its value depends on whoever issues and backs it",
68
+ DEP_KNOWN_ISSUER: (d) => "Issued by " + d.issuer + ": it can mint, freeze or move tokens, like any regulated issuer. You trust the issuer",
67
69
  DEP_NONE: () => "Does not depend on an issuer or a bridge"
68
70
  },
69
71
  offSolana: " and its main market may be outside Solana",
package/src/i18n/es.js CHANGED
@@ -38,6 +38,7 @@ export default {
38
38
  CTL_NON_TRANSFERABLE: () => "No se puede transferir",
39
39
  CTL_DEFAULT_FROZEN: () => "Las cuentas nuevas pueden crearse congeladas",
40
40
  CTL_ONLY_PROGRAMS: () => "Solo hay autoridades controladas por un programa (ver dependencia)",
41
+ CTL_KNOWN_ISSUER: (d) => "Lo controla su emisor, " + d.issuer + " (ver dependencia)",
41
42
  CTL_NONE: () => "Sin autoridades activas: nadie puede crear más ni congelar",
42
43
  CTL_SERIAL_CREATOR: (d) => "El creador ha lanzado " + d.n + " tokens más en las últimas 24 h (según RugCheck): es un lanzador en serie, que suele abandonar cada token en cuanto llegan compradores",
43
44
  BRAND_COPY: (d) => "Usa el nombre de " + d.brand + ", una empresa real que no tiene token en Solana: no es oficial, casi seguro una imitación para atraer compradores",
@@ -67,6 +68,7 @@ export default {
67
68
  DEP_PROGRAM_AUTHORITY: () => "Un programa controla el mint o el freeze (puente o protocolo): hay que confiar en su código",
68
69
  DEP_PERMANENT_DELEGATE: () => "El emisor puede mover o quemar tus tokens (delegado permanente): confías en el emisor",
69
70
  DEP_STABLE_ISSUER: () => "Su valor depende de quien la emite y la respalda",
71
+ DEP_KNOWN_ISSUER: (d) => "Lo emite " + d.issuer + ": puede crear, congelar o mover tokens, como cualquier emisor regulado. Confías en el emisor",
70
72
  DEP_NONE: () => "No depende de un emisor ni de un puente"
71
73
  },
72
74
  offSolana: " y su mercado principal puede estar fuera de Solana",
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import { estimatePnl } from "./pnl.js";
5
5
  import { takeSnapshots } from "./changes.js";
6
6
  import { confluence, commonHolders } from "./confluence.js";
7
7
  import { getTokens } from "./tokens.js";
8
+ import { loadWalletBase } from "./base.js";
8
9
 
9
10
  /**
10
11
  * The easy entry point: one object with every high-level analysis bound to one network client.
@@ -35,7 +36,8 @@ export function createScanner(opts) {
35
36
  },
36
37
  takeSnapshots: (wallets, o) => takeSnapshots(client, wallets, o),
37
38
  confluence: (wallets, o) => confluence(client, wallets, o),
38
- commonHolders: (mints, o) => commonHolders(client, mints, o)
39
+ commonHolders: (mints, o) => commonHolders(client, mints, o),
40
+ loadWalletBase: (url) => loadWalletBase(client, url)
39
41
  };
40
42
  }
41
43
 
@@ -46,7 +48,7 @@ export { isOnCurve, onCurve, b58bytes } from "./curve.js";
46
48
  export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf } from "./tokens.js";
47
49
  export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair, brandOf, copiesOf, fakeLiquidity, FAKE_LIQ_RATIO } from "./markets.js";
48
50
  export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches, CREATOR_DEAD_MCAP } from "./rugcheck.js";
49
- export { score, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
51
+ export { score, issuerOf, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
50
52
  export { recentLaunches } from "./launch.js";
51
53
  export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
52
54
  export { holderBase, markBusy, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders,
@@ -54,4 +56,5 @@ export { holderBase, markBusy, findFunder, findFunders, clusterByFunder, holderR
54
56
  export { fetchSwaps, solHistory, priceSeries, parseTrades, pnlFromTrades, pnlStatus, estimatePnl } from "./pnl.js";
55
57
  export { capSnap, chgDiff, chgAgg, takeSnapshots, diffSnapshots } from "./changes.js";
56
58
  export { confluence, commonHolders } from "./confluence.js";
59
+ export { loadWalletBase, knownType, WALLET_TYPES } from "./base.js";
57
60
  export { describe, explainLight, lightLabel, levelLabel, missingText, typeInfo, describeStep, describeNote, describeError, format, addLanguage } from "./i18n/index.js";
package/src/markets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEX, DEX_SEARCH, GECKO, USDC, USDT, WSOL, FAMOUS, BRANDS } from "./constants.js";
1
+ import { DEX, DEX_SEARCH, GECKO, USDC, USDT, WSOL, FAMOUS, BRANDS, brandKey, BRAND_MATURE_DAYS, BRAND_MATURE_LIQ } from "./constants.js";
2
2
  import { ScanError, isCancel } from "./errors.js";
3
3
  import { sleep } from "./util.js";
4
4
 
@@ -38,16 +38,18 @@ export function fakeLiquidity(pair) {
38
38
  }
39
39
 
40
40
  // A real company's name on a token that is not issued by anyone: {brand, exact}. exact = the name or the
41
- // symbol is just the brand ("Spotify"); otherwise the brand is one word of a longer name ("Spotify Cat")
42
- export function brandOf(pair, type) {
41
+ // symbol is just the brand ("Spotify"); otherwise the brand is one word of a longer name ("Spotify Cat").
42
+ // Established markets (see BRAND_MATURE_DAYS) are left alone
43
+ export function brandOf(pair, type, now) {
43
44
  if (type === "TOKENIZED" || type === "BRIDGED" || type === "STABLE") return null;
45
+ const liq = (pair && pair.liquidity && pair.liquidity.usd) || 0;
46
+ const days = (pair && pair.pairCreatedAt) ? ((now || Date.now()) - pair.pairCreatedAt) / 86400000 : 0;
47
+ if (days >= BRAND_MATURE_DAYS && liq >= BRAND_MATURE_LIQ) return null;
44
48
  const b = (pair && pair.baseToken) || {};
45
- const clean = function (s) { return String(s || "").toLowerCase().replace(/[$'’.\-_]/g, " ").replace(/\s+/g, " ").trim(); };
46
- const name = clean(b.name), sym = clean(b.symbol);
47
- for (const k of Object.keys(BRANDS)) {
48
- const w = k.replace(/_/g, " ");
49
- if (name === w || sym === w || name.replace(/ /g, "") === w.replace(/ /g, "")) return { brand: BRANDS[k], exact: true };
50
- if ((" " + name + " ").indexOf(" " + w + " ") >= 0) return { brand: BRANDS[k], exact: false };
49
+ const name = brandKey(b.name), sym = brandKey(b.symbol);
50
+ for (const w of Object.keys(BRANDS)) {
51
+ if (name === w || sym === w || name.replace(/ /g, "") === w.replace(/ /g, "")) return { brand: BRANDS[w], exact: true };
52
+ if ((" " + name + " ").indexOf(" " + w + " ") >= 0) return { brand: BRANDS[w], exact: false };
51
53
  }
52
54
  return null;
53
55
  }