solana-risk-engine 0.4.0 → 0.4.2
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 +1 -1
- package/package.json +1 -1
- package/src/holders.js +76 -6
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +1 -0
- package/src/index.js +2 -1
- package/src/tokens.js +103 -103
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ 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, hotGroups, creator, ... }`. 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 350 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. |
|
|
77
77
|
| `estimatePnl(wallets)` | `{ pnl: { [wallet]: { realized, unreal, total, ret, st, open } } }`. Needs Helius. |
|
|
78
78
|
| `takeSnapshots(wallets)` | Today's holdings. Store them yourself and pass them to `diffSnapshots(prev, next)`. |
|
|
79
79
|
| `confluence(wallets, { pct, withPnl, pnlFilter })` | Tokens held in common, with risk. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/holders.js
CHANGED
|
@@ -64,7 +64,13 @@ export async function findFunder(client, addr) {
|
|
|
64
64
|
}
|
|
65
65
|
if (!reachedEnd || !sigs || !sigs.length) return null;
|
|
66
66
|
const earliest = sigs[sigs.length - 1].signature;
|
|
67
|
-
|
|
67
|
+
// version 1 is needed for recent transactions; an older RPC that does not know it yet gets 0
|
|
68
|
+
let tx;
|
|
69
|
+
try { tx = await client.rpc("getTransaction", [earliest, { maxSupportedTransactionVersion: 1, encoding: "jsonParsed" }]); }
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (isCancel(e) || !/version|invalid param/i.test(e.message || "")) throw e;
|
|
72
|
+
tx = await client.rpc("getTransaction", [earliest, { maxSupportedTransactionVersion: 0, encoding: "jsonParsed" }]);
|
|
73
|
+
}
|
|
68
74
|
const keys = tx && tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys;
|
|
69
75
|
if (!keys || !keys.length) return null;
|
|
70
76
|
const payer = keys[0] && (keys[0].pubkey !== undefined ? keys[0].pubkey : keys[0]);
|
|
@@ -83,6 +89,44 @@ export async function findFunders(client, wallets, onStep) {
|
|
|
83
89
|
return out;
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// Exchanges and launch platforms move thousands of transactions an hour; the busiest farms and
|
|
93
|
+
// distributors seen in the 28/10 study did 1-150. Above this, a wallet is infrastructure, not a person.
|
|
94
|
+
export const EXCHANGE_PER_HOUR = 350;
|
|
95
|
+
// Young tokens (under 14 days) are read for the team's split, mature ones for what stayed hidden
|
|
96
|
+
export const YOUNG_DAYS = 14;
|
|
97
|
+
// A group (holders sharing the source of their first SOL) over this % of the supply warns
|
|
98
|
+
export const HOLD_GROUP_YOUNG = 10, HOLD_GROUP_MATURE = 15;
|
|
99
|
+
|
|
100
|
+
// Transactions per hour over a page of signatures (newest first)
|
|
101
|
+
export function perHour(sigs) {
|
|
102
|
+
if (!sigs || sigs.length < 2) return 0;
|
|
103
|
+
const span = ((sigs[0].blockTime || 0) - (sigs[sigs.length - 1].blockTime || 0)) / 3600;
|
|
104
|
+
return span > 0 ? sigs.length / span : sigs.length * 3600;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Rate of each source wallet (one request each)
|
|
108
|
+
export async function sourceRates(client, addrs, onStep) {
|
|
109
|
+
const out = {};
|
|
110
|
+
let done = 0;
|
|
111
|
+
await pool(addrs, 3, async function (a) {
|
|
112
|
+
try { out[a] = perHour(await client.rpc("getSignaturesForAddress", [a, { limit: 1000 }])); }
|
|
113
|
+
catch (e) { if (isCancel(e)) throw e; out[a] = null; }
|
|
114
|
+
done++; if (onStep) onStep(done, addrs.length);
|
|
115
|
+
});
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Groups by size of their share of the supply, biggest first: [{id, funder, members, pct}]
|
|
120
|
+
export function groupList(clusters, rows) {
|
|
121
|
+
const by = {};
|
|
122
|
+
rows.forEach(function (r) {
|
|
123
|
+
const c = clusters[r.owner]; if (!c) return;
|
|
124
|
+
const g = by[c.id] || (by[c.id] = { id: c.id, funder: c.funder, members: [], pct: 0 });
|
|
125
|
+
g.members.push(r.owner); g.pct += r.pct;
|
|
126
|
+
});
|
|
127
|
+
return Object.keys(by).map(function (k) { return by[k]; }).sort(function (a, b) { return b.pct - a.pct; });
|
|
128
|
+
}
|
|
129
|
+
|
|
86
130
|
// An exchange pays the withdrawals of thousands of customers from the same wallet: if that is the
|
|
87
131
|
// source, the "group" joins strangers. So sources that pass the same very-active test used to separate
|
|
88
132
|
// bots are ignored (hot = markBusy result over the sources).
|
|
@@ -153,7 +197,14 @@ export async function creatorInfo(client, mint, onProgress) {
|
|
|
153
197
|
* Whale finder: top holders, with bots/exchanges separated from people, people grouped by who funded
|
|
154
198
|
* them, and the creator's history. Works best with your own RPC (it needs getTokenLargestAccounts).
|
|
155
199
|
*
|
|
200
|
+
* Young tokens (under 14 days: market age from DexScreener, else RugCheck's detectedAt) also trace the
|
|
201
|
+
* busy wallets: on day one most big holders trade a lot and the team's split hides among them. A group
|
|
202
|
+
* over 10% of the supply in a young token (bad) or 15% in a mature one (warn) is a HOLD_GROUP signal.
|
|
203
|
+
*
|
|
156
204
|
* @param [opts.creator=true] also look up the creator and insider networks on RugCheck
|
|
205
|
+
* @param [opts.createdAt] ms when its market started, if the caller knows it
|
|
206
|
+
* @param [opts.now]
|
|
207
|
+
* @returns {..., mode: "young"|"mature", ageDays, groups, signals}
|
|
157
208
|
*/
|
|
158
209
|
export async function analyzeHolders(client, mint, opts) {
|
|
159
210
|
opts = opts || {};
|
|
@@ -163,18 +214,36 @@ export async function analyzeHolders(client, mint, opts) {
|
|
|
163
214
|
const wal = b.uniq.filter(function (o) { return b.kind[o] === "wallet"; });
|
|
164
215
|
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 }); });
|
|
165
216
|
const hr = holderRows(b, busy), pair = b.pair;
|
|
166
|
-
const
|
|
217
|
+
const now = opts.now || Date.now();
|
|
218
|
+
let created = opts.createdAt || (pair && pair.pairCreatedAt) || null;
|
|
219
|
+
if (!created) {
|
|
220
|
+
const rc = await rugcheckReport(client, mint);
|
|
221
|
+
if (rc && rc.detectedAt) created = Date.parse(rc.detectedAt) || null;
|
|
222
|
+
}
|
|
223
|
+
const ageDays = created ? Math.max(0, (now - created) / 86400000) : null;
|
|
224
|
+
const mode = ageDays !== null && ageDays < YOUNG_DAYS ? "young" : "mature";
|
|
225
|
+
// who is traced: people always; in young tokens also the busy wallets
|
|
226
|
+
const traced = hr.rows.filter(function (r) { return r.kind === "wallet" || (mode === "young" && r.kind === "active"); })
|
|
227
|
+
.map(function (r) { return r.owner; });
|
|
167
228
|
let clusters = {}, hotGroups = 0;
|
|
168
|
-
if (
|
|
169
|
-
const funders = await findFunders(client,
|
|
229
|
+
if (traced.length >= 2) {
|
|
230
|
+
const funders = await findFunders(client, traced, function (c, n) { progress(onProgress, "funders", 0.8 + 0.08 * c / Math.max(1, n), { done: c, total: n }); });
|
|
170
231
|
clusters = clusterByFunder(funders);
|
|
171
232
|
const srcs = uniq(Object.keys(clusters).map(function (a) { return clusters[a].funder; }));
|
|
172
233
|
if (srcs.length) {
|
|
173
|
-
|
|
234
|
+
// a source is an exchange when it moves more than EXCHANGE_PER_HOUR transactions an hour (the bot
|
|
235
|
+
// test, 200 in 2 days, also caught distributors doing ~50 an hour and hid their groups)
|
|
236
|
+
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 }); });
|
|
237
|
+
const hot = {};
|
|
238
|
+
srcs.forEach(function (f) { if (rates[f] > EXCHANGE_PER_HOUR) hot[f] = true; });
|
|
174
239
|
hotGroups = srcs.filter(function (f) { return hot[f]; }).length;
|
|
175
240
|
if (hotGroups) clusters = clusterByFunder(funders, hot);
|
|
176
241
|
}
|
|
177
242
|
}
|
|
243
|
+
const groups = groupList(clusters, hr.rows), signals = [];
|
|
244
|
+
const limit = mode === "young" ? HOLD_GROUP_YOUNG : HOLD_GROUP_MATURE;
|
|
245
|
+
if (groups[0] && groups[0].pct > limit)
|
|
246
|
+
signals.push({ code: "HOLD_GROUP", s: mode === "young" ? "bad" : "warn", data: { n: groups[0].members.length, pct: groups[0].pct, id: groups[0].id, funder: groups[0].funder } });
|
|
178
247
|
let creator = null;
|
|
179
248
|
if (opts.creator !== false) {
|
|
180
249
|
progress(onProgress, "creator", 0.9);
|
|
@@ -184,6 +253,7 @@ export async function analyzeHolders(client, mint, opts) {
|
|
|
184
253
|
return {
|
|
185
254
|
mint: mint, sym: (pair && pair.baseToken && pair.baseToken.symbol) || short(mint),
|
|
186
255
|
name: (pair && pair.baseToken && pair.baseToken.name) || "", price: hr.price, rows: hr.rows,
|
|
187
|
-
mintAuth: !!b.info.mintAuth, freezeAuth: !!b.info.freezeAuth, clusters: clusters, hotGroups: hotGroups, creator: creator
|
|
256
|
+
mintAuth: !!b.info.mintAuth, freezeAuth: !!b.info.freezeAuth, clusters: clusters, hotGroups: hotGroups, creator: creator,
|
|
257
|
+
mode: mode, ageDays: ageDays, groups: groups, signals: signals
|
|
188
258
|
};
|
|
189
259
|
}
|
package/src/i18n/en.js
CHANGED
|
@@ -37,6 +37,7 @@ export default {
|
|
|
37
37
|
CTL_ONLY_PROGRAMS: () => "Only program-controlled authorities (see dependency)",
|
|
38
38
|
CTL_NONE: () => "No active authorities: nobody can mint more or freeze",
|
|
39
39
|
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
|
+
HOLD_GROUP: (d) => d.n + " holders got their first SOL from the same wallet and together hold " + pct(d.pct) + " of the supply: most likely one person or team",
|
|
40
41
|
CTL_CREATOR_HISTORY: (d) => "Of the " + d.old + " tokens the creator launched before (over 24 h ago), " + d.dead + " stayed under $10,000 market cap (per RugCheck): their launches rarely take off",
|
|
41
42
|
IMPOSTOR: (d) => "Impersonates " + d.symbol + ": the real " + d.symbol + " has another address (" + short(d.real) + "). It is a copy, almost certainly a scam",
|
|
42
43
|
|
package/src/i18n/es.js
CHANGED
|
@@ -40,6 +40,7 @@ export default {
|
|
|
40
40
|
CTL_ONLY_PROGRAMS: () => "Solo hay autoridades controladas por un programa (ver dependencia)",
|
|
41
41
|
CTL_NONE: () => "Sin autoridades activas: nadie puede crear más ni congelar",
|
|
42
42
|
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
|
+
HOLD_GROUP: (d) => d.n + " holders recibieron su primer SOL de la misma wallet y juntos tienen el " + pct(d.pct) + " del suministro: probablemente son la misma persona o equipo",
|
|
43
44
|
CTL_CREATOR_HISTORY: (d) => "De los " + d.old + " tokens que el creador lanzó antes (hace más de 24 h), " + d.dead + " se quedaron por debajo de 10.000 $ de capitalización (según RugCheck): sus lanzamientos no suelen despegar",
|
|
44
45
|
IMPOSTOR: (d) => "Se hace pasar por " + d.symbol + ": el " + d.symbol + " de verdad tiene otra dirección (" + short(d.real) + "). Es una copia, casi seguro una estafa",
|
|
45
46
|
|
package/src/index.js
CHANGED
|
@@ -49,7 +49,8 @@ export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLau
|
|
|
49
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";
|
|
50
50
|
export { recentLaunches } from "./launch.js";
|
|
51
51
|
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
52
|
-
export { holderBase, markBusy, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders
|
|
52
|
+
export { holderBase, markBusy, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders,
|
|
53
|
+
perHour, sourceRates, groupList, EXCHANGE_PER_HOUR, YOUNG_DAYS, HOLD_GROUP_YOUNG, HOLD_GROUP_MATURE } from "./holders.js";
|
|
53
54
|
export { fetchSwaps, solHistory, priceSeries, parseTrades, pnlFromTrades, pnlStatus, estimatePnl } from "./pnl.js";
|
|
54
55
|
export { capSnap, chgDiff, chgAgg, takeSnapshots, diffSnapshots } from "./changes.js";
|
|
55
56
|
export { confluence, commonHolders } from "./confluence.js";
|
package/src/tokens.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
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
|
-
return {
|
|
38
|
-
mintAuth: p.mintAuthority || null, freezeAuth: p.freezeAuthority || null,
|
|
39
|
-
supply: Number(p.supply || 0), decimals: Number(p.decimals || 0),
|
|
40
|
-
ext: (p.extensions || []).map(function (e) { return e.extension; })
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// A key off the ed25519 curve has no private key: only a program can sign for it (a PDA), whoever
|
|
45
|
-
// owns its account. PDAs that hold SOL (stake pool and AMM authorities) are owned by the System
|
|
46
|
-
// Program like any wallet, so the owner alone would count them as people.
|
|
47
|
-
export function kindOf(key, account, missingIsWallet) {
|
|
48
|
-
if (!onCurve(b58bytes(key))) return "program";
|
|
49
|
-
if (account) return account.owner === SYSTEM_PROGRAM ? "wallet" : "program";
|
|
50
|
-
return missingIsWallet ? "wallet" : "program";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// "wallet" or "program" for each address
|
|
54
|
-
export async function accountKinds(client, addrs, missingIsWallet) {
|
|
55
|
-
if (!addrs.length) return {};
|
|
56
|
-
const r = await client.rpc("getMultipleAccounts", [addrs, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }]);
|
|
57
|
-
const out = {};
|
|
58
|
-
addrs.forEach(function (k, i) { out[k] = kindOf(k, r.value[i], missingIsWallet); });
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Who controls the mint and freeze authorities: a person's wallet or a program (bridge, protocol)
|
|
63
|
-
export async function getAuthKinds(client, info) {
|
|
64
|
-
const keys = [];
|
|
65
|
-
[info.mintAuth, info.freezeAuth].forEach(function (a) { if (a && keys.indexOf(a) < 0) keys.push(a); });
|
|
66
|
-
if (!keys.length) return {};
|
|
67
|
-
return accountKinds(client, keys, true); // no account but on the curve: an empty wallet
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Concentration of the supply. Each large account is traced to its owner: a pool or a program
|
|
71
|
-
// holding tokens is not a whale, so w1/w10 count only people's wallets.
|
|
72
|
-
export async function getConc(client, mint, supply) {
|
|
73
|
-
const r = await client.rpc("getTokenLargestAccounts", [mint]);
|
|
74
|
-
const v = (r && r.value) || [];
|
|
75
|
-
if (!v.length || supply <= 0) throw new Error("no holder data");
|
|
76
|
-
const amts = v.map(function (x) { return Number(x.amount); });
|
|
77
|
-
const out = {
|
|
78
|
-
ok: false,
|
|
79
|
-
top1: amts[0] / supply * 100,
|
|
80
|
-
top10: amts.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100
|
|
81
|
-
};
|
|
82
|
-
const addrs = v.map(function (x) { return x.address; });
|
|
83
|
-
if (addrs.some(function (x) { return !x; })) return out;
|
|
84
|
-
try {
|
|
85
|
-
const ta = await client.rpc("getMultipleAccounts", [addrs, { encoding: "jsonParsed" }]);
|
|
86
|
-
const owners = ta.value.map(function (x) {
|
|
87
|
-
return x && x.data && x.data.parsed && x.data.parsed.info && x.data.parsed.info.owner || null;
|
|
88
|
-
});
|
|
89
|
-
if (owners.some(function (o) { return !o; })) return out;
|
|
90
|
-
const kind = await accountKinds(client, uniq(owners), false);
|
|
91
|
-
const agg = {};
|
|
92
|
-
owners.forEach(function (o, i) { agg[o] = (agg[o] || 0) + amts[i]; });
|
|
93
|
-
const ws = [];
|
|
94
|
-
let prog = 0;
|
|
95
|
-
Object.keys(agg).forEach(function (o) { if (kind[o] === "wallet") ws.push(agg[o]); else prog += agg[o]; });
|
|
96
|
-
ws.sort(function (x, y) { return y - x; });
|
|
97
|
-
out.ok = true; out.nW = ws.length;
|
|
98
|
-
out.w1 = ws.length ? ws[0] / supply * 100 : 0;
|
|
99
|
-
out.w10 = ws.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100;
|
|
100
|
-
out.prog = prog / supply * 100;
|
|
101
|
-
} catch (e) { if (isCancel(e)) throw e; }
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
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
|
+
return {
|
|
38
|
+
mintAuth: p.mintAuthority || null, freezeAuth: p.freezeAuthority || null,
|
|
39
|
+
supply: Number(p.supply || 0), decimals: Number(p.decimals || 0),
|
|
40
|
+
ext: (p.extensions || []).map(function (e) { return e.extension; })
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A key off the ed25519 curve has no private key: only a program can sign for it (a PDA), whoever
|
|
45
|
+
// owns its account. PDAs that hold SOL (stake pool and AMM authorities) are owned by the System
|
|
46
|
+
// Program like any wallet, so the owner alone would count them as people.
|
|
47
|
+
export function kindOf(key, account, missingIsWallet) {
|
|
48
|
+
if (!onCurve(b58bytes(key))) return "program";
|
|
49
|
+
if (account) return account.owner === SYSTEM_PROGRAM ? "wallet" : "program";
|
|
50
|
+
return missingIsWallet ? "wallet" : "program";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// "wallet" or "program" for each address
|
|
54
|
+
export async function accountKinds(client, addrs, missingIsWallet) {
|
|
55
|
+
if (!addrs.length) return {};
|
|
56
|
+
const r = await client.rpc("getMultipleAccounts", [addrs, { encoding: "base64", dataSlice: { offset: 0, length: 0 } }]);
|
|
57
|
+
const out = {};
|
|
58
|
+
addrs.forEach(function (k, i) { out[k] = kindOf(k, r.value[i], missingIsWallet); });
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Who controls the mint and freeze authorities: a person's wallet or a program (bridge, protocol)
|
|
63
|
+
export async function getAuthKinds(client, info) {
|
|
64
|
+
const keys = [];
|
|
65
|
+
[info.mintAuth, info.freezeAuth].forEach(function (a) { if (a && keys.indexOf(a) < 0) keys.push(a); });
|
|
66
|
+
if (!keys.length) return {};
|
|
67
|
+
return accountKinds(client, keys, true); // no account but on the curve: an empty wallet
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Concentration of the supply. Each large account is traced to its owner: a pool or a program
|
|
71
|
+
// holding tokens is not a whale, so w1/w10 count only people's wallets.
|
|
72
|
+
export async function getConc(client, mint, supply) {
|
|
73
|
+
const r = await client.rpc("getTokenLargestAccounts", [mint]);
|
|
74
|
+
const v = (r && r.value) || [];
|
|
75
|
+
if (!v.length || supply <= 0) throw new Error("no holder data");
|
|
76
|
+
const amts = v.map(function (x) { return Number(x.amount); });
|
|
77
|
+
const out = {
|
|
78
|
+
ok: false,
|
|
79
|
+
top1: amts[0] / supply * 100,
|
|
80
|
+
top10: amts.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100
|
|
81
|
+
};
|
|
82
|
+
const addrs = v.map(function (x) { return x.address; });
|
|
83
|
+
if (addrs.some(function (x) { return !x; })) return out;
|
|
84
|
+
try {
|
|
85
|
+
const ta = await client.rpc("getMultipleAccounts", [addrs, { encoding: "jsonParsed" }]);
|
|
86
|
+
const owners = ta.value.map(function (x) {
|
|
87
|
+
return x && x.data && x.data.parsed && x.data.parsed.info && x.data.parsed.info.owner || null;
|
|
88
|
+
});
|
|
89
|
+
if (owners.some(function (o) { return !o; })) return out;
|
|
90
|
+
const kind = await accountKinds(client, uniq(owners), false);
|
|
91
|
+
const agg = {};
|
|
92
|
+
owners.forEach(function (o, i) { agg[o] = (agg[o] || 0) + amts[i]; });
|
|
93
|
+
const ws = [];
|
|
94
|
+
let prog = 0;
|
|
95
|
+
Object.keys(agg).forEach(function (o) { if (kind[o] === "wallet") ws.push(agg[o]); else prog += agg[o]; });
|
|
96
|
+
ws.sort(function (x, y) { return y - x; });
|
|
97
|
+
out.ok = true; out.nW = ws.length;
|
|
98
|
+
out.w1 = ws.length ? ws[0] / supply * 100 : 0;
|
|
99
|
+
out.w10 = ws.slice(0, 10).reduce(function (s, x) { return s + x; }, 0) / supply * 100;
|
|
100
|
+
out.prog = prog / supply * 100;
|
|
101
|
+
} catch (e) { if (isCancel(e)) throw e; }
|
|
102
|
+
return out;
|
|
103
|
+
}
|