solana-risk-engine 0.4.3 → 0.4.4

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 CHANGED
@@ -73,7 +73,8 @@ Methods (all accept `{ onProgress(step, fraction) }`):
73
73
  | `analyzeToken(mint, { skipCreator })` | `{ lvl, sev, type, lights, reasons, impostor, pair, creator, ... }` |
74
74
  | `analyzeWallet(wallet, { minUsd })` | `{ sol, balances, total, hidden, results: [token results] }` |
75
75
  | `analyzeAddress(addr)` | `{ kind: "token" \| "wallet", ... }`, detected automatically |
76
- | `analyzeHolders(mint)` | `{ rows, clusters, groups, hotGroups, creator, mode, ageDays, signals, ... }`. Young tokens (under 14 days) also trace busy wallets; a source over 100 tx/h counts as an exchange; a group over 10% (young) or 15% (mature) of the supply gives a `HOLD_GROUP` signal. Best with your own RPC. |
76
+ | `analyzeHolders(mint)` | `{ rows, clusters, groups, hotGroups, creator, mode, ageDays, signals, ... }`. Young tokens (under 14 days) also trace busy wallets; a source over 100 tx/h counts as an exchange; a group over 10% (young) or 15% (mature) of the supply gives a `HOLD_GROUP` signal. With `{ base: true }` it also reads [SolFlag's wallet base](https://solflag.com/datos/base.json) (exchanges, launch farms, distributors and serial creators its collector has seen): known exchanges are recognised without measuring them, and rows and group sources get `known` / `knownSources`. Best with your own RPC. |
77
+ | `loadWalletBase([url])` | SolFlag's wallet base `{ wallets: { addr: { t, ph, n, roles } } }`, or `null` if it cannot be read. Kept 10 minutes. `knownType(base, addr)` gives a wallet's type. |
77
78
  | `estimatePnl(wallets)` | `{ pnl: { [wallet]: { realized, unreal, total, ret, st, open } } }`. Needs Helius. |
78
79
  | `takeSnapshots(wallets)` | Today's holdings. Store them yourself and pass them to `diffSnapshots(prev, next)`. |
79
80
  | `confluence(wallets, { pct, withPnl, pnlFilter })` | Tokens held in common, with risk. |
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "solana-risk-engine",
3
- "version": "0.4.3",
4
- "description": "Explainable risk analysis for Solana tokens and wallets: creator control, real holder concentration, inflated liquidity, impostor tokens, funder clusters and PnL. Zero dependencies, runs in Node and the browser.",
5
- "type": "module",
6
- "main": "./src/index.js",
7
- "exports": {
8
- ".": "./src/index.js",
9
- "./i18n": "./src/i18n/index.js"
10
- },
11
- "files": [
12
- "src",
13
- "README.md",
14
- "LICENSE",
15
- "bin"
16
- ],
17
- "bin": {
18
- "solana-risk": "bin/solana-risk.js"
19
- },
20
- "scripts": {
21
- "test": "node --test test/*.test.js",
22
- "example": "node bin/solana-risk.js"
23
- },
24
- "engines": {
25
- "node": ">=18"
26
- },
27
- "keywords": [
28
- "solana",
29
- "risk",
30
- "rug-pull",
31
- "scam-detection",
32
- "token",
33
- "spl-token",
34
- "token-2022",
35
- "holders",
36
- "dexscreener",
37
- "rugcheck"
38
- ],
39
- "license": "MIT",
40
- "author": "panpontomate",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/panpontomate/solana-risk-engine.git"
44
- },
45
- "homepage": "https://github.com/panpontomate/solana-risk-engine#readme",
46
- "bugs": "https://github.com/panpontomate/solana-risk-engine/issues"
47
- }
1
+ {
2
+ "name": "solana-risk-engine",
3
+ "version": "0.4.4",
4
+ "description": "Explainable risk analysis for Solana tokens and wallets: creator control, real holder concentration, inflated liquidity, impostor tokens, funder clusters and PnL. Zero dependencies, runs in Node and the browser.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./i18n": "./src/i18n/index.js"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "README.md",
14
+ "LICENSE",
15
+ "bin"
16
+ ],
17
+ "bin": {
18
+ "solana-risk": "bin/solana-risk.js"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test test/*.test.js",
22
+ "example": "node bin/solana-risk.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "keywords": [
28
+ "solana",
29
+ "risk",
30
+ "rug-pull",
31
+ "scam-detection",
32
+ "token",
33
+ "spl-token",
34
+ "token-2022",
35
+ "holders",
36
+ "dexscreener",
37
+ "rugcheck"
38
+ ],
39
+ "license": "MIT",
40
+ "author": "panpontomate",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/panpontomate/solana-risk-engine.git"
44
+ },
45
+ "homepage": "https://github.com/panpontomate/solana-risk-engine#readme",
46
+ "bugs": "https://github.com/panpontomate/solana-risk-engine/issues"
47
+ }
package/src/base.js ADDED
@@ -0,0 +1,28 @@
1
+ import { WALLET_BASE_URL } from "./constants.js";
2
+
3
+ // SolFlag's wallet base: what its collector learns from every launch it checks (exchanges, launch farms,
4
+ // distributors, serial creators), refreshed every 30 minutes. A known exchange is recognised at once,
5
+ // without asking the RPC how many transactions it makes
6
+
7
+ export const WALLET_TYPES = ["exchange", "granja", "repartidor", "creador en serie", "sniper"];
8
+ const BASE_TTL = 600000;
9
+ const caches = new WeakMap();
10
+
11
+ // The base, or null when it cannot be read (it is an extra: nothing depends on it). Each client keeps it
12
+ // 10 minutes per URL
13
+ export async function loadWalletBase(client, url) {
14
+ url = url || WALLET_BASE_URL;
15
+ if (!caches.has(client)) caches.set(client, {});
16
+ const cache = caches.get(client), c = cache[url];
17
+ if (c && Date.now() - c.at < BASE_TTL) return c.data;
18
+ const j = await client.softJson(url, 8000);
19
+ const data = j && j.wallets && typeof j.wallets === "object" ? j : null;
20
+ cache[url] = { at: Date.now(), data: data };
21
+ return data;
22
+ }
23
+
24
+ // Type of a wallet in the base ("exchange", "granja"...) or null
25
+ export function knownType(base, addr) {
26
+ const w = base && base.wallets && base.wallets[addr];
27
+ return (w && WALLET_TYPES.indexOf(w.t) >= 0) ? w.t : null;
28
+ }
package/src/constants.js CHANGED
@@ -5,6 +5,8 @@
5
5
  export const PUBLIC_RPC = "https://solana-rpc.publicnode.com";
6
6
  export const VIBE_RPC = "https://public.rpc.solanavibestation.com";
7
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";
8
10
  export const DEX_SEARCH = "https://api.dexscreener.com/latest/dex/search?q=";
9
11
  export const RUGCHECK = "https://api.rugcheck.xyz/v1/tokens/";
10
12
  export const GECKO = "https://api.geckoterminal.com/api/v2/networks/solana/";
@@ -60,6 +62,19 @@ export const BRANDS = {
60
62
  adidas: "Adidas", gucci: "Gucci", louis_vuitton: "Louis Vuitton", rolex: "Rolex", spacex: "SpaceX", neuralink: "Neuralink"
61
63
  };
62
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
+
63
78
  // Token-2022 extensions that add risk, with the points they add to the no-market spam score
64
79
  export const EXT_POINTS = {
65
80
  permanentDelegate: 25,
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/risk.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EXT_POINTS, RANKS, PUMP_CURVE_DEXES } from "./constants.js";
1
+ import { EXT_POINTS, RANKS, PUMP_CURVE_DEXES, ISSUERS } from "./constants.js";
2
2
  import { isCancel } from "./errors.js";
3
3
  import { short, sleep } from "./util.js";
4
4
  import { getMint, getAuthKinds, getConc } from "./tokens.js";
@@ -16,8 +16,10 @@ function sig(code, s, data) {
16
16
  const DAY = 86400000;
17
17
  // Other tokens launched by the same creator in the last 24 h that make it a serial launcher
18
18
  export const SERIAL_WARN = 2, SERIAL_BAD = 5;
19
- // Creator track record: at least HISTORY_MIN older tokens and this share of them never took off
20
- export const HISTORY_MIN = 3, HISTORY_WARN = 0.8, HISTORY_BAD_MIN = 10, HISTORY_BAD = 0.9;
19
+ // Creator track record: at least HISTORY_MIN older tokens and this share of them never took off. Red needs
20
+ // 95%: 86% of all new tokens never reach $10k (149-token sample, 28/10), so one launch that took off out of
21
+ // ten is already better than chance and must not raise the verdict
22
+ export const HISTORY_MIN = 3, HISTORY_WARN = 0.8, HISTORY_BAD_MIN = 10, HISTORY_BAD = 0.95;
21
23
 
22
24
  /* ---------- old point score: only for tokens without a market, to spot spam ---------- */
23
25
  export function score(info) {
@@ -58,6 +60,23 @@ export function tokenType(info, pair, now) {
58
60
  return "OPEN";
59
61
  }
60
62
 
63
+ /* ---------- known issuers ---------- */
64
+ // Name of the known issuer that holds every control key of this mint, or null. Needs the extension keys
65
+ // read by getMint (extKeys): without them a permanent delegate or a hook could belong to anyone
66
+ export function issuerOf(info) {
67
+ if (!info || !info.extKeys) return null;
68
+ const keys = [info.mintAuth, info.freezeAuth].concat(info.extKeys).filter(Boolean);
69
+ if (!keys.length) return null;
70
+ const found = ISSUERS.find(function (i) { return keys.every(function (k) { return i.keys.indexOf(k) >= 0; }); });
71
+ return found ? found.name : null;
72
+ }
73
+
74
+ // Extensions that only matter as they are set: a hook with no program and nobody able to add one does
75
+ // nothing, and new accounts are only born frozen when the default state says "frozen". Mint info without
76
+ // those details (older callers) keeps the old behaviour: the extension alone is enough
77
+ function hookActive(info) { return !info.hook || !!(info.hook.program || info.hook.auth); }
78
+ function bornFrozen(info) { return !info.defaultState || info.defaultState === "frozen"; }
79
+
61
80
  /* ---------- the four traffic lights ---------- */
62
81
  function mkLight(k, items, okSignal) {
63
82
  if (!items.length) return { k: k, s: "ok", items: [okSignal] };
@@ -81,18 +100,22 @@ function mkLight(k, items, okSignal) {
81
100
  export function buildLights(info, conc, pair, value, type, rc, now, launches) {
82
101
  now = now || Date.now();
83
102
  const ak = (info && info.authKind) || {};
103
+ const issuer = issuerOf(info);
84
104
  let it;
85
105
 
86
106
  // 1) creator control
87
107
  it = [];
88
108
  if (!info) it.push(sig("NO_INFO", "warn"));
89
109
  else {
90
- if (info.mintAuth && ak[info.mintAuth] !== "program") it.push(sig("CTL_MINT_ACTIVE", "bad"));
91
- if (info.freezeAuth && ak[info.freezeAuth] !== "program") it.push(sig("CTL_FREEZE_ACTIVE", "warn"));
92
- if (info.ext.indexOf("transferHook") >= 0) it.push(sig("CTL_TRANSFER_HOOK", "warn"));
110
+ // a known issuer's mint, freeze, hook and pause are how the product works (see dependency)
111
+ if (!issuer) {
112
+ if (info.mintAuth && ak[info.mintAuth] !== "program") it.push(sig("CTL_MINT_ACTIVE", "bad"));
113
+ if (info.freezeAuth && ak[info.freezeAuth] !== "program") it.push(sig("CTL_FREEZE_ACTIVE", "warn"));
114
+ if (info.ext.indexOf("transferHook") >= 0 && hookActive(info)) it.push(sig("CTL_TRANSFER_HOOK", "warn"));
115
+ if (info.ext.indexOf("defaultAccountState") >= 0 && bornFrozen(info)) it.push(sig("CTL_DEFAULT_FROZEN", "warn"));
116
+ }
93
117
  if (info.ext.indexOf("transferFeeConfig") >= 0) it.push(sig("CTL_TRANSFER_FEE", "warn"));
94
118
  if (info.ext.indexOf("nonTransferable") >= 0) it.push(sig("CTL_NON_TRANSFERABLE", "warn"));
95
- if (info.ext.indexOf("defaultAccountState") >= 0) it.push(sig("CTL_DEFAULT_FROZEN", "warn"));
96
119
  }
97
120
  // Launch farms put out a token every few minutes and drop each one as soon as buyers arrive: in a
98
121
  // 100-token Pump.fun sample one wallet launched 7 copies of "GMS" in 10 minutes
@@ -102,7 +125,8 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
102
125
  const bad = launches.old >= HISTORY_BAD_MIN && launches.dead / launches.old >= HISTORY_BAD;
103
126
  it.push(sig("CTL_CREATOR_HISTORY", bad ? "bad" : "warn", { old: launches.old, dead: launches.dead, best: launches.best, creator: launches.creator }));
104
127
  }
105
- const control = mkLight("control", it, sig((info && (info.mintAuth || info.freezeAuth)) ? "CTL_ONLY_PROGRAMS" : "CTL_NONE", "ok"));
128
+ const control = mkLight("control", it, issuer ? sig("CTL_KNOWN_ISSUER", "ok", { issuer: issuer })
129
+ : sig((info && (info.mintAuth || info.freezeAuth)) ? "CTL_ONLY_PROGRAMS" : "CTL_NONE", "ok"));
106
130
 
107
131
  // 2) concentration in people (pools and programs do not count)
108
132
  let concentration;
@@ -150,7 +174,8 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
150
174
 
151
175
  // 4) dependency on an issuer or a bridge
152
176
  it = [];
153
- if (info) {
177
+ if (issuer) it.push(sig("DEP_KNOWN_ISSUER", "warn", { issuer: issuer }));
178
+ else if (info) {
154
179
  if ((info.mintAuth && ak[info.mintAuth] === "program") || (info.freezeAuth && ak[info.freezeAuth] === "program"))
155
180
  it.push(sig("DEP_PROGRAM_AUTHORITY", "warn"));
156
181
  if (info.ext.indexOf("permanentDelegate") >= 0) it.push(sig("DEP_PERMANENT_DELEGATE", "warn"));
package/src/tokens.js CHANGED
@@ -34,13 +34,33 @@ export async function getMint(client, mint) {
34
34
  const r = await client.rpc("getAccountInfo", [mint, { encoding: "jsonParsed" }]);
35
35
  const p = r && r.value && r.value.data && r.value.data.parsed && r.value.data.parsed.info;
36
36
  if (!p) return null;
37
+ const exts = p.extensions || [];
38
+ const state = function (name) { const e = exts.find(function (x) { return x.extension === name; }); return (e && e.state) || {}; };
39
+ const hook = state("transferHook");
37
40
  return {
38
41
  mintAuth: p.mintAuthority || null, freezeAuth: p.freezeAuthority || null,
39
42
  supply: Number(p.supply || 0), decimals: Number(p.decimals || 0),
40
- ext: (p.extensions || []).map(function (e) { return e.extension; })
43
+ ext: exts.map(function (e) { return e.extension; }),
44
+ // what the extensions are set to: a transfer hook with no program and no authority does nothing, and
45
+ // "default account state" only matters when new accounts are born frozen
46
+ hook: { program: hook.programId || null, auth: hook.authority || null },
47
+ defaultState: state("defaultAccountState").accountState || null,
48
+ // every key that controls something in the extensions (delegate, hook, pause, metadata...)
49
+ extKeys: extKeys(exts)
41
50
  };
42
51
  }
43
52
 
53
+ function extKeys(exts) {
54
+ const out = [];
55
+ exts.forEach(function (e) {
56
+ Object.keys(e.state || {}).forEach(function (k) {
57
+ const v = e.state[k];
58
+ if (typeof v === "string" && /authority$|^delegate$|^programId$/i.test(k) && out.indexOf(v) < 0) out.push(v);
59
+ });
60
+ });
61
+ return out;
62
+ }
63
+
44
64
  // A key off the ed25519 curve has no private key: only a program can sign for it (a PDA), whoever
45
65
  // owns its account. PDAs that hold SOL (stake pool and AMM authorities) are owned by the System
46
66
  // Program like any wallet, so the owner alone would count them as people.