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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-risk-engine",
3
- "version": "0.4.6",
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) { kind[o] = "active"; busy[o] = Math.max(1, Math.round((t1 - t0) / 3600)); }
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) => "Impersonates " + d.symbol + ": the real " + d.symbol + " has another address (" + short(d.real) + "). It is a copy, almost certainly a scam",
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) => "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",
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";