solana-risk-engine 0.4.2 → 0.4.3
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 +144 -144
- package/package.json +47 -47
- package/src/client.js +157 -143
- package/src/constants.js +78 -62
- package/src/holders.js +260 -259
- package/src/i18n/en.js +112 -109
- package/src/i18n/es.js +115 -112
- package/src/index.js +57 -57
- package/src/markets.js +136 -87
- package/src/risk.js +268 -255
- package/src/wallet.js +94 -94
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
|
+
}
|