solana-risk-engine 0.4.6 → 0.4.8
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/holders.js +38 -2
- package/src/i18n/en.js +1 -1
- package/src/i18n/es.js +1 -1
- package/src/index.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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
|
@@ -31,15 +31,23 @@ export async function holderBase(client, mint, onProgress) {
|
|
|
31
31
|
// People's wallets rarely move hundreds of transactions in a couple of days; exchanges, bots and
|
|
32
32
|
// market makers do. Checked with the last 200 signatures. Marks them kind[o] = "active" (mutates kind)
|
|
33
33
|
// and returns {address: hours it took to do those 200}.
|
|
34
|
+
// Signatures also list what a wallet only RECEIVES (fee shares, airdrops, dust), so before calling it busy
|
|
35
|
+
// a few of those transactions are read: if it signed none, it is a quiet wallet that gets paid, not a bot
|
|
36
|
+
// (31/10: the 60% holder of a pump.fun quote token got ~1,500 fee shares an hour and signed nothing).
|
|
37
|
+
// Those stay "wallet" and are listed in busy.passive (not enumerable) as {address: true}.
|
|
34
38
|
export async function markBusy(client, kind, wallets, onStep) {
|
|
35
|
-
const busy = {};
|
|
39
|
+
const busy = {}, passive = {};
|
|
40
|
+
Object.defineProperty(busy, "passive", { value: passive });
|
|
36
41
|
let chk = 0;
|
|
37
42
|
await pool(wallets, 3, async function (o) {
|
|
38
43
|
try {
|
|
39
44
|
const sg = await client.rpc("getSignaturesForAddress", [o, { limit: 200 }]);
|
|
40
45
|
if (sg && sg.length >= 200) {
|
|
41
46
|
const t1 = sg[0].blockTime, t0 = sg[sg.length - 1].blockTime;
|
|
42
|
-
if (t1 && t0 && (t1 - t0) < 2 * 86400) {
|
|
47
|
+
if (t1 && t0 && (t1 - t0) < 2 * 86400) {
|
|
48
|
+
if (await onlyReceives(client, o, sg)) passive[o] = true;
|
|
49
|
+
else { kind[o] = "active"; busy[o] = Math.max(1, Math.round((t1 - t0) / 3600)); }
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
} catch (e) { if (isCancel(e)) throw e; }
|
|
45
53
|
chk++; if (onStep) onStep(chk, wallets.length);
|
|
@@ -47,6 +55,33 @@ export async function markBusy(client, kind, wallets, onStep) {
|
|
|
47
55
|
return busy;
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
// How many of the busy list's transactions are read to tell a wallet that acts from one that only receives
|
|
59
|
+
export const PASSIVE_SAMPLE = 4;
|
|
60
|
+
|
|
61
|
+
function signers(tx) {
|
|
62
|
+
const m = tx && tx.transaction && tx.transaction.message;
|
|
63
|
+
const keys = (m && m.accountKeys) || [];
|
|
64
|
+
if (keys.length && typeof keys[0] === "object") return keys.filter(function (k) { return k.signer; }).map(function (k) { return k.pubkey; });
|
|
65
|
+
const n = (m && m.header && m.header.numRequiredSignatures) || 1; // raw format: signers come first
|
|
66
|
+
return keys.slice(0, n);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// true only when at least 2 of the sampled transactions could be read and the wallet signed none of them
|
|
70
|
+
export async function onlyReceives(client, o, sg) {
|
|
71
|
+
const idx = [];
|
|
72
|
+
for (let i = 0; i < PASSIVE_SAMPLE; i++) idx.push(Math.round(i * (sg.length - 1) / Math.max(1, PASSIVE_SAMPLE - 1)));
|
|
73
|
+
let read = 0;
|
|
74
|
+
for (const i of uniq(idx)) {
|
|
75
|
+
let tx = null;
|
|
76
|
+
try { tx = await client.rpc("getTransaction", [sg[i].signature, { maxSupportedTransactionVersion: 1, encoding: "jsonParsed" }]); }
|
|
77
|
+
catch (e) { if (isCancel(e)) throw e; }
|
|
78
|
+
if (!tx) continue;
|
|
79
|
+
read++;
|
|
80
|
+
if (signers(tx).indexOf(o) >= 0) return false;
|
|
81
|
+
}
|
|
82
|
+
return read >= 2;
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
/* ---------- wallets funded from the same source (maybe one owner split into several accounts) ---------- */
|
|
51
86
|
// By Solana protocol, account 0 of any transaction is always the fee payer. Looking at who paid the
|
|
52
87
|
// very first transaction of each wallet shows who "created" it with the first SOL transfer. If several
|
|
@@ -156,6 +191,7 @@ export function holderRows(b, busy) {
|
|
|
156
191
|
b.largest.forEach(function (a, i) {
|
|
157
192
|
const o = b.owners[i] || a.address;
|
|
158
193
|
if (!agg[o]) agg[o] = { owner: o, kind: b.kind[o] || "program", raw: 0, busy: busy[o] || 0 };
|
|
194
|
+
if (busy.passive && busy.passive[o]) agg[o].passive = true; // very busy, but only receiving
|
|
159
195
|
agg[o].raw += Number(a.amount);
|
|
160
196
|
});
|
|
161
197
|
const price = Number((b.pair && b.pair.priceUsd) || 0);
|
package/src/i18n/en.js
CHANGED
|
@@ -43,7 +43,7 @@ export default {
|
|
|
43
43
|
COPYCAT: (d) => "There " + (d.n === 1 ? "is another token" : "are " + d.n + " more tokens") + " with the same name and ticker; the oldest one with a real market (" + short(d.original) + ", " + d.days + (d.days === 1 ? " day" : " days") + " old) is not this one: check you have the right address",
|
|
44
44
|
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",
|
|
45
45
|
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",
|
|
46
|
-
IMPOSTOR: (d) => "
|
|
46
|
+
IMPOSTOR: (d) => "Not the original " + d.symbol + ". Make sure it is the token you want",
|
|
47
47
|
|
|
48
48
|
CON_SKIPPED: () => "Holder concentration not checked",
|
|
49
49
|
CON_UNAVAILABLE: () => "Could not check holder concentration (the service did not answer; try again in a while)",
|
package/src/i18n/es.js
CHANGED
|
@@ -46,7 +46,7 @@ export default {
|
|
|
46
46
|
COPYCAT: (d) => "Hay " + (d.n === 1 ? "otro token" : d.n + " tokens más") + " con el mismo nombre y ticker; el más antiguo con mercado real (" + short(d.original) + ", de hace " + d.days + (d.days === 1 ? " día" : " días") + ") no es este: comprueba que tienes la dirección correcta",
|
|
47
47
|
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",
|
|
48
48
|
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",
|
|
49
|
-
IMPOSTOR: (d) => "
|
|
49
|
+
IMPOSTOR: (d) => "No es el " + d.symbol + " original. Comprueba que es el token que buscas",
|
|
50
50
|
|
|
51
51
|
CON_SKIPPED: () => "Sin comprobar la concentración de holders",
|
|
52
52
|
CON_UNAVAILABLE: () => "No se pudo comprobar la concentración de holders (el servicio no respondió; prueba otra vez en un rato)",
|
package/src/index.js
CHANGED
|
@@ -51,7 +51,7 @@ export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLau
|
|
|
51
51
|
export { score, issuerOf, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
|
|
52
52
|
export { recentLaunches } from "./launch.js";
|
|
53
53
|
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
54
|
-
export { holderBase, markBusy, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders,
|
|
54
|
+
export { holderBase, markBusy, onlyReceives, PASSIVE_SAMPLE, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders,
|
|
55
55
|
perHour, sourceRates, groupList, EXCHANGE_PER_HOUR, YOUNG_DAYS, HOLD_GROUP_YOUNG, HOLD_GROUP_MATURE } from "./holders.js";
|
|
56
56
|
export { fetchSwaps, solHistory, priceSeries, parseTrades, pnlFromTrades, pnlStatus, estimatePnl } from "./pnl.js";
|
|
57
57
|
export { capSnap, chgDiff, chgAgg, takeSnapshots, diffSnapshots } from "./changes.js";
|