solana-risk-engine 0.4.2 → 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/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.
package/src/wallet.js CHANGED
@@ -1,94 +1,94 @@
1
- import { ADDRESS_RE, KNOWN, LEVELS, PUMP_CURVE_DEXES } from "./constants.js";
2
- import { ScanError } from "./errors.js";
3
- import { pool, progress } from "./util.js";
4
- import { getTokens, isMint } from "./tokens.js";
5
- import { getMarkets, geckoLiquidity, geckoPair } from "./markets.js";
6
- import { assess } from "./risk.js";
7
-
8
- function checkAddress(addr) {
9
- if (!ADDRESS_RE.test(addr || "")) throw new ScanError("INVALID_ADDRESS", "not a Solana address");
10
- }
11
-
12
- export function sortResults(list) {
13
- return list.sort(function (a, b) {
14
- return LEVELS.indexOf(a.lvl) - LEVELS.indexOf(b.lvl) || (b.sev || 0) - (a.sev || 0) || b.pts - a.pts || b.value - a.value;
15
- });
16
- }
17
-
18
- /**
19
- * Risk of a single token. Throws IS_NFT for NFTs.
20
- * @param [opts.noGecko] do not fill DexScreener's Pump.fun gaps from GeckoTerminal (also for analyzeWallet)
21
- */
22
- export async function analyzeToken(client, mint, opts) {
23
- opts = opts || {};
24
- checkAddress(mint);
25
- progress(opts.onProgress, "markets", 0.15);
26
- const mkr = await getMarkets(client, [mint]);
27
- // Pump.fun gaps in DexScreener: not listed yet (seconds old) or $0 liquidity on the bonding curve
28
- if (!opts.noGecko) {
29
- if (!mkr.data[mint] && !mkr.failed.length) { const gp = await geckoPair(client, mint); if (gp) mkr.data[mint] = gp; }
30
- await geckoLiquidity(client, mkr.data[mint]);
31
- }
32
- progress(opts.onProgress, "token", 0.45);
33
- const r = await assess(client, mint, mkr.data[mint] || null, { marketUnknown: mkr.failed.length > 0, skipConcentration: opts.skipConcentration, skipCreator: opts.skipCreator });
34
- if (!r) throw new ScanError("IS_NFT", "that address is an NFT, not a token");
35
- progress(opts.onProgress, "done", 1);
36
- return r;
37
- }
38
-
39
- /**
40
- * Risk of every token a wallet holds.
41
- *
42
- * @param [opts.minUsd=1] hide priced tokens worth less than this (dust)
43
- * @param [opts.concurrency=3]
44
- * @returns {wallet, sol, balances, total, hidden, results}
45
- */
46
- export async function analyzeWallet(client, wallet, opts) {
47
- opts = opts || {};
48
- checkAddress(wallet);
49
- const minUsd = opts.minUsd === undefined ? 1 : opts.minUsd;
50
- const onProgress = opts.onProgress;
51
- progress(onProgress, "wallet", 0.02);
52
- const w = await getTokens(client, wallet);
53
- const mints = Object.keys(w.tokens);
54
- const res = { wallet: wallet, sol: w.sol, balances: w.tokens, total: mints.length, hidden: 0, results: [] };
55
- if (!mints.length) return res;
56
- progress(onProgress, "markets", 0.08);
57
- const mkr = await getMarkets(client, mints, function (d, n) { progress(onProgress, "markets", 0.08 + 0.12 * d / n, { done: d, total: n }); });
58
- const mk = mkr.data, failedMk = {};
59
- mkr.failed.forEach(function (m) { failedMk[m] = 1; });
60
- // Bonding-curve tokens show $0 liquidity on DexScreener; ask GeckoTerminal for a few (its free API
61
- // allows ~30 requests a minute, and wallets full of airdropped spam would use them up)
62
- if (!opts.noGecko) {
63
- const curve = mints.filter(function (m) { const p = mk[m]; return p && PUMP_CURVE_DEXES.indexOf(p.dexId) >= 0 && !(p.liquidity && p.liquidity.usd); }).slice(0, 10);
64
- await pool(curve, 2, async function (m) { await geckoLiquidity(client, mk[m]); });
65
- }
66
-
67
- const out = [], todo = [];
68
- mints.forEach(function (m) {
69
- const pair = mk[m], price = Number((pair && pair.priceUsd) || 0);
70
- let value = w.tokens[m] * price;
71
- if (pair && pair.liqRaw) value = Math.min(value, pair.liquidity.usd); // with inflated liquidity the price is fake too
72
- if (KNOWN[m]) { out.push({ mint: m, sym: KNOWN[m], name: "", pts: 0, sev: 0, lvl: "LOW", known: true, type: null, lights: null, reasons: [{ code: "KNOWN_TOKEN", s: "ok" }], value: value, pair: pair, img: null }); return; }
73
- if (price > 0 && value < minUsd) { res.hidden++; return; }
74
- todo.push({ mint: m, pair: pair, value: value, unknown: !!failedMk[m] });
75
- });
76
-
77
- let done = 0;
78
- await pool(todo, opts.concurrency || 3, async function (t) {
79
- const r = await assess(client, t.mint, t.pair, { value: t.value, marketUnknown: t.unknown, skipConcentration: opts.skipConcentration, skipCreator: opts.skipCreator });
80
- if (r) out.push(r);
81
- done++;
82
- progress(onProgress, "tokens", 0.2 + 0.8 * done / Math.max(1, todo.length), { done: done, total: todo.length });
83
- });
84
- res.results = sortResults(out);
85
- return res;
86
- }
87
-
88
- /** Detects whether the address is a token mint or a wallet and analyses it accordingly. */
89
- export async function analyzeAddress(client, addr, opts) {
90
- checkAddress(addr);
91
- progress(opts && opts.onProgress, "address", 0.01);
92
- if (await isMint(client, addr)) return { kind: "token", token: await analyzeToken(client, addr, opts) };
93
- return { kind: "wallet", wallet: await analyzeWallet(client, addr, opts) };
94
- }
1
+ import { ADDRESS_RE, KNOWN, LEVELS, PUMP_CURVE_DEXES } from "./constants.js";
2
+ import { ScanError } from "./errors.js";
3
+ import { pool, progress } from "./util.js";
4
+ import { getTokens, isMint } from "./tokens.js";
5
+ import { getMarkets, geckoLiquidity, geckoPair } from "./markets.js";
6
+ import { assess } from "./risk.js";
7
+
8
+ function checkAddress(addr) {
9
+ if (!ADDRESS_RE.test(addr || "")) throw new ScanError("INVALID_ADDRESS", "not a Solana address");
10
+ }
11
+
12
+ export function sortResults(list) {
13
+ return list.sort(function (a, b) {
14
+ return LEVELS.indexOf(a.lvl) - LEVELS.indexOf(b.lvl) || (b.sev || 0) - (a.sev || 0) || b.pts - a.pts || b.value - a.value;
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Risk of a single token. Throws IS_NFT for NFTs.
20
+ * @param [opts.noGecko] do not fill DexScreener's Pump.fun gaps from GeckoTerminal (also for analyzeWallet)
21
+ */
22
+ export async function analyzeToken(client, mint, opts) {
23
+ opts = opts || {};
24
+ checkAddress(mint);
25
+ progress(opts.onProgress, "markets", 0.15);
26
+ const mkr = await getMarkets(client, [mint]);
27
+ // Pump.fun gaps in DexScreener: not listed yet (seconds old) or $0 liquidity on the bonding curve
28
+ if (!opts.noGecko) {
29
+ if (!mkr.data[mint] && !mkr.failed.length) { const gp = await geckoPair(client, mint); if (gp) mkr.data[mint] = gp; }
30
+ await geckoLiquidity(client, mkr.data[mint]);
31
+ }
32
+ progress(opts.onProgress, "token", 0.45);
33
+ const r = await assess(client, mint, mkr.data[mint] || null, { marketUnknown: mkr.failed.length > 0, skipConcentration: opts.skipConcentration, skipCreator: opts.skipCreator, copies: opts.copies !== false });
34
+ if (!r) throw new ScanError("IS_NFT", "that address is an NFT, not a token");
35
+ progress(opts.onProgress, "done", 1);
36
+ return r;
37
+ }
38
+
39
+ /**
40
+ * Risk of every token a wallet holds.
41
+ *
42
+ * @param [opts.minUsd=1] hide priced tokens worth less than this (dust)
43
+ * @param [opts.concurrency=3]
44
+ * @returns {wallet, sol, balances, total, hidden, results}
45
+ */
46
+ export async function analyzeWallet(client, wallet, opts) {
47
+ opts = opts || {};
48
+ checkAddress(wallet);
49
+ const minUsd = opts.minUsd === undefined ? 1 : opts.minUsd;
50
+ const onProgress = opts.onProgress;
51
+ progress(onProgress, "wallet", 0.02);
52
+ const w = await getTokens(client, wallet);
53
+ const mints = Object.keys(w.tokens);
54
+ const res = { wallet: wallet, sol: w.sol, balances: w.tokens, total: mints.length, hidden: 0, results: [] };
55
+ if (!mints.length) return res;
56
+ progress(onProgress, "markets", 0.08);
57
+ const mkr = await getMarkets(client, mints, function (d, n) { progress(onProgress, "markets", 0.08 + 0.12 * d / n, { done: d, total: n }); });
58
+ const mk = mkr.data, failedMk = {};
59
+ mkr.failed.forEach(function (m) { failedMk[m] = 1; });
60
+ // Bonding-curve tokens show $0 liquidity on DexScreener; ask GeckoTerminal for a few (its free API
61
+ // allows ~30 requests a minute, and wallets full of airdropped spam would use them up)
62
+ if (!opts.noGecko) {
63
+ const curve = mints.filter(function (m) { const p = mk[m]; return p && PUMP_CURVE_DEXES.indexOf(p.dexId) >= 0 && !(p.liquidity && p.liquidity.usd); }).slice(0, 10);
64
+ await pool(curve, 2, async function (m) { await geckoLiquidity(client, mk[m]); });
65
+ }
66
+
67
+ const out = [], todo = [];
68
+ mints.forEach(function (m) {
69
+ const pair = mk[m], price = Number((pair && pair.priceUsd) || 0);
70
+ let value = w.tokens[m] * price;
71
+ if (pair && pair.liqRaw) value = Math.min(value, pair.liquidity.usd); // with inflated liquidity the price is fake too
72
+ if (KNOWN[m]) { out.push({ mint: m, sym: KNOWN[m], name: "", pts: 0, sev: 0, lvl: "LOW", known: true, type: null, lights: null, reasons: [{ code: "KNOWN_TOKEN", s: "ok" }], value: value, pair: pair, img: null }); return; }
73
+ if (price > 0 && value < minUsd) { res.hidden++; return; }
74
+ todo.push({ mint: m, pair: pair, value: value, unknown: !!failedMk[m] });
75
+ });
76
+
77
+ let done = 0;
78
+ await pool(todo, opts.concurrency || 3, async function (t) {
79
+ const r = await assess(client, t.mint, t.pair, { value: t.value, marketUnknown: t.unknown, skipConcentration: opts.skipConcentration, skipCreator: opts.skipCreator });
80
+ if (r) out.push(r);
81
+ done++;
82
+ progress(onProgress, "tokens", 0.2 + 0.8 * done / Math.max(1, todo.length), { done: done, total: todo.length });
83
+ });
84
+ res.results = sortResults(out);
85
+ return res;
86
+ }
87
+
88
+ /** Detects whether the address is a token mint or a wallet and analyses it accordingly. */
89
+ export async function analyzeAddress(client, addr, opts) {
90
+ checkAddress(addr);
91
+ progress(opts && opts.onProgress, "address", 0.01);
92
+ if (await isMint(client, addr)) return { kind: "token", token: await analyzeToken(client, addr, opts) };
93
+ return { kind: "wallet", wallet: await analyzeWallet(client, addr, opts) };
94
+ }