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/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
+ }