solana-risk-engine 0.4.5 → 0.4.7
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/package.json +1 -1
- package/src/base.js +28 -28
- package/src/client.js +157 -157
- package/src/constants.js +4 -0
- package/src/holders.js +310 -274
- package/src/i18n/en.js +114 -114
- package/src/i18n/es.js +117 -117
- package/src/index.js +60 -60
- package/src/markets.js +141 -138
- package/src/tokens.js +123 -123
- package/src/wallet.js +94 -94
package/src/tokens.js
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
import { TOKEN_PROGRAM, TOKEN_2022, SYSTEM_PROGRAM } from "./constants.js";
|
|
2
|
-
import { isCancel } from "./errors.js";
|
|
3
|
-
import { onCurve, b58bytes } from "./curve.js";
|
|
4
|
-
import { uniq } from "./util.js";
|
|
5
|
-
|
|
6
|
-
// SOL balance and every token the wallet holds (classic SPL + Token-2022), summed by mint
|
|
7
|
-
export async function getTokens(client, wallet) {
|
|
8
|
-
const res = await Promise.all([
|
|
9
|
-
client.rpc("getBalance", [wallet]),
|
|
10
|
-
client.rpc("getTokenAccountsByOwner", [wallet, { programId: TOKEN_PROGRAM }, { encoding: "jsonParsed" }]),
|
|
11
|
-
client.rpc("getTokenAccountsByOwner", [wallet, { programId: TOKEN_2022 }, { encoding: "jsonParsed" }])
|
|
12
|
-
]);
|
|
13
|
-
const tokens = {};
|
|
14
|
-
[res[1], res[2]].forEach(function (r) {
|
|
15
|
-
r.value.forEach(function (c) {
|
|
16
|
-
const info = c.account.data.parsed.info;
|
|
17
|
-
const q = Number(info.tokenAmount.uiAmount || 0);
|
|
18
|
-
if (q > 0) tokens[info.mint] = (tokens[info.mint] || 0) + q;
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
return { sol: res[0].value / 1e9, tokens: tokens };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// A mint account belongs to a token program; a wallet belongs to the System Program
|
|
25
|
-
export async function isMint(client, addr) {
|
|
26
|
-
try {
|
|
27
|
-
const r = await client.rpc("getAccountInfo", [addr, { encoding: "jsonParsed" }]);
|
|
28
|
-
const v = r && r.value;
|
|
29
|
-
return !!(v && (v.owner === TOKEN_PROGRAM || v.owner === TOKEN_2022) && v.data && v.data.parsed && v.data.parsed.type === "mint");
|
|
30
|
-
} catch (e) { if (isCancel(e)) throw e; return false; } // if it cannot be known, treat it as a wallet
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function getMint(client, mint) {
|
|
34
|
-
const r = await client.rpc("getAccountInfo", [mint, { encoding: "jsonParsed" }]);
|
|
35
|
-
const p = r && r.value && r.value.data && r.value.data.parsed && r.value.data.parsed.info;
|
|
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");
|
|
40
|
-
return {
|
|
41
|
-
mintAuth: p.mintAuthority || null, freezeAuth: p.freezeAuthority || null,
|
|
42
|
-
supply: Number(p.supply || 0), decimals: Number(p.decimals || 0),
|
|
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)
|
|
50
|
-
};
|
|
51
|
-
}
|
|
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
|
-
|
|
64
|
-
// A key off the ed25519 curve has no private key: only a program can sign for it (a PDA), whoever
|
|
65
|
-
// owns its account. PDAs that hold SOL (stake pool and AMM authorities) are owned by the System
|
|
66
|
-
// Program like any wallet, so the owner alone would count them as people.
|
|
67
|
-
export function kindOf(key, account, missingIsWallet) {
|
|
68
|
-
if (!onCurve(b58bytes(key))) return "program";
|
|
69
|
-
if (account) return account.owner === SYSTEM_PROGRAM ? "wallet" : "program";
|
|
70
|
-
return missingIsWallet ? "wallet" : "program";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// "wallet" or "program" for each address
|
|
74
|
-
export async function accountKinds(client, addrs, missingIsWallet) {
|
|
75
|
-
if (!addrs.length) return {};
|
|
76
|
-
const r = await client.rpc("getMultipleAccounts", [addrs, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }]);
|
|
77
|
-
const out = {};
|
|
78
|
-
addrs.forEach(function (k, i) { out[k] = kindOf(k, r.value[i], missingIsWallet); });
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Who controls the mint and freeze authorities: a person's wallet or a program (bridge, protocol)
|
|
83
|
-
export async function getAuthKinds(client, info) {
|
|
84
|
-
const keys = [];
|
|
85
|
-
[info.mintAuth, info.freezeAuth].forEach(function (a) { if (a && keys.indexOf(a) < 0) keys.push(a); });
|
|
86
|
-
if (!keys.length) return {};
|
|
87
|
-
return accountKinds(client, keys, true); // no account but on the curve: an empty wallet
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Concentration of the supply. Each large account is traced to its owner: a pool or a program
|
|
91
|
-
// holding tokens is not a whale, so w1/w10 count only people's wallets.
|
|
92
|
-
export async function getConc(client, mint, supply) {
|
|
93
|
-
const r = await client.rpc("getTokenLargestAccounts", [mint]);
|
|
94
|
-
const v = (r && r.value) || [];
|
|
95
|
-
if (!v.length || supply <= 0) throw new Error("no holder data");
|
|
96
|
-
const amts = v.map(function (x) { return Number(x.amount); });
|
|
97
|
-
const out = {
|
|
98
|
-
ok: false,
|
|
99
|
-
top1: amts[0] / supply * 100,
|
|
100
|
-
top10: amts.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100
|
|
101
|
-
};
|
|
102
|
-
const addrs = v.map(function (x) { return x.address; });
|
|
103
|
-
if (addrs.some(function (x) { return !x; })) return out;
|
|
104
|
-
try {
|
|
105
|
-
const ta = await client.rpc("getMultipleAccounts", [addrs, { encoding: "jsonParsed" }]);
|
|
106
|
-
const owners = ta.value.map(function (x) {
|
|
107
|
-
return x && x.data && x.data.parsed && x.data.parsed.info && x.data.parsed.info.owner || null;
|
|
108
|
-
});
|
|
109
|
-
if (owners.some(function (o) { return !o; })) return out;
|
|
110
|
-
const kind = await accountKinds(client, uniq(owners), false);
|
|
111
|
-
const agg = {};
|
|
112
|
-
owners.forEach(function (o, i) { agg[o] = (agg[o] || 0) + amts[i]; });
|
|
113
|
-
const ws = [];
|
|
114
|
-
let prog = 0;
|
|
115
|
-
Object.keys(agg).forEach(function (o) { if (kind[o] === "wallet") ws.push(agg[o]); else prog += agg[o]; });
|
|
116
|
-
ws.sort(function (x, y) { return y - x; });
|
|
117
|
-
out.ok = true; out.nW = ws.length;
|
|
118
|
-
out.w1 = ws.length ? ws[0] / supply * 100 : 0;
|
|
119
|
-
out.w10 = ws.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100;
|
|
120
|
-
out.prog = prog / supply * 100;
|
|
121
|
-
} catch (e) { if (isCancel(e)) throw e; }
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
1
|
+
import { TOKEN_PROGRAM, TOKEN_2022, SYSTEM_PROGRAM } from "./constants.js";
|
|
2
|
+
import { isCancel } from "./errors.js";
|
|
3
|
+
import { onCurve, b58bytes } from "./curve.js";
|
|
4
|
+
import { uniq } from "./util.js";
|
|
5
|
+
|
|
6
|
+
// SOL balance and every token the wallet holds (classic SPL + Token-2022), summed by mint
|
|
7
|
+
export async function getTokens(client, wallet) {
|
|
8
|
+
const res = await Promise.all([
|
|
9
|
+
client.rpc("getBalance", [wallet]),
|
|
10
|
+
client.rpc("getTokenAccountsByOwner", [wallet, { programId: TOKEN_PROGRAM }, { encoding: "jsonParsed" }]),
|
|
11
|
+
client.rpc("getTokenAccountsByOwner", [wallet, { programId: TOKEN_2022 }, { encoding: "jsonParsed" }])
|
|
12
|
+
]);
|
|
13
|
+
const tokens = {};
|
|
14
|
+
[res[1], res[2]].forEach(function (r) {
|
|
15
|
+
r.value.forEach(function (c) {
|
|
16
|
+
const info = c.account.data.parsed.info;
|
|
17
|
+
const q = Number(info.tokenAmount.uiAmount || 0);
|
|
18
|
+
if (q > 0) tokens[info.mint] = (tokens[info.mint] || 0) + q;
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return { sol: res[0].value / 1e9, tokens: tokens };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A mint account belongs to a token program; a wallet belongs to the System Program
|
|
25
|
+
export async function isMint(client, addr) {
|
|
26
|
+
try {
|
|
27
|
+
const r = await client.rpc("getAccountInfo", [addr, { encoding: "jsonParsed" }]);
|
|
28
|
+
const v = r && r.value;
|
|
29
|
+
return !!(v && (v.owner === TOKEN_PROGRAM || v.owner === TOKEN_2022) && v.data && v.data.parsed && v.data.parsed.type === "mint");
|
|
30
|
+
} catch (e) { if (isCancel(e)) throw e; return false; } // if it cannot be known, treat it as a wallet
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getMint(client, mint) {
|
|
34
|
+
const r = await client.rpc("getAccountInfo", [mint, { encoding: "jsonParsed" }]);
|
|
35
|
+
const p = r && r.value && r.value.data && r.value.data.parsed && r.value.data.parsed.info;
|
|
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");
|
|
40
|
+
return {
|
|
41
|
+
mintAuth: p.mintAuthority || null, freezeAuth: p.freezeAuthority || null,
|
|
42
|
+
supply: Number(p.supply || 0), decimals: Number(p.decimals || 0),
|
|
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)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
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
|
+
|
|
64
|
+
// A key off the ed25519 curve has no private key: only a program can sign for it (a PDA), whoever
|
|
65
|
+
// owns its account. PDAs that hold SOL (stake pool and AMM authorities) are owned by the System
|
|
66
|
+
// Program like any wallet, so the owner alone would count them as people.
|
|
67
|
+
export function kindOf(key, account, missingIsWallet) {
|
|
68
|
+
if (!onCurve(b58bytes(key))) return "program";
|
|
69
|
+
if (account) return account.owner === SYSTEM_PROGRAM ? "wallet" : "program";
|
|
70
|
+
return missingIsWallet ? "wallet" : "program";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// "wallet" or "program" for each address
|
|
74
|
+
export async function accountKinds(client, addrs, missingIsWallet) {
|
|
75
|
+
if (!addrs.length) return {};
|
|
76
|
+
const r = await client.rpc("getMultipleAccounts", [addrs, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }]);
|
|
77
|
+
const out = {};
|
|
78
|
+
addrs.forEach(function (k, i) { out[k] = kindOf(k, r.value[i], missingIsWallet); });
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Who controls the mint and freeze authorities: a person's wallet or a program (bridge, protocol)
|
|
83
|
+
export async function getAuthKinds(client, info) {
|
|
84
|
+
const keys = [];
|
|
85
|
+
[info.mintAuth, info.freezeAuth].forEach(function (a) { if (a && keys.indexOf(a) < 0) keys.push(a); });
|
|
86
|
+
if (!keys.length) return {};
|
|
87
|
+
return accountKinds(client, keys, true); // no account but on the curve: an empty wallet
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Concentration of the supply. Each large account is traced to its owner: a pool or a program
|
|
91
|
+
// holding tokens is not a whale, so w1/w10 count only people's wallets.
|
|
92
|
+
export async function getConc(client, mint, supply) {
|
|
93
|
+
const r = await client.rpc("getTokenLargestAccounts", [mint]);
|
|
94
|
+
const v = (r && r.value) || [];
|
|
95
|
+
if (!v.length || supply <= 0) throw new Error("no holder data");
|
|
96
|
+
const amts = v.map(function (x) { return Number(x.amount); });
|
|
97
|
+
const out = {
|
|
98
|
+
ok: false,
|
|
99
|
+
top1: amts[0] / supply * 100,
|
|
100
|
+
top10: amts.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100
|
|
101
|
+
};
|
|
102
|
+
const addrs = v.map(function (x) { return x.address; });
|
|
103
|
+
if (addrs.some(function (x) { return !x; })) return out;
|
|
104
|
+
try {
|
|
105
|
+
const ta = await client.rpc("getMultipleAccounts", [addrs, { encoding: "jsonParsed" }]);
|
|
106
|
+
const owners = ta.value.map(function (x) {
|
|
107
|
+
return x && x.data && x.data.parsed && x.data.parsed.info && x.data.parsed.info.owner || null;
|
|
108
|
+
});
|
|
109
|
+
if (owners.some(function (o) { return !o; })) return out;
|
|
110
|
+
const kind = await accountKinds(client, uniq(owners), false);
|
|
111
|
+
const agg = {};
|
|
112
|
+
owners.forEach(function (o, i) { agg[o] = (agg[o] || 0) + amts[i]; });
|
|
113
|
+
const ws = [];
|
|
114
|
+
let prog = 0;
|
|
115
|
+
Object.keys(agg).forEach(function (o) { if (kind[o] === "wallet") ws.push(agg[o]); else prog += agg[o]; });
|
|
116
|
+
ws.sort(function (x, y) { return y - x; });
|
|
117
|
+
out.ok = true; out.nW = ws.length;
|
|
118
|
+
out.w1 = ws.length ? ws[0] / supply * 100 : 0;
|
|
119
|
+
out.w10 = ws.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100;
|
|
120
|
+
out.prog = prog / supply * 100;
|
|
121
|
+
} catch (e) { if (isCancel(e)) throw e; }
|
|
122
|
+
return out;
|
|
123
|
+
}
|
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, 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
|
-
}
|
|
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
|
+
}
|