solana-risk-engine 0.4.11 → 0.4.13
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/i18n/en.js +1 -0
- package/src/i18n/es.js +121 -120
- package/src/index.js +1 -1
- package/src/launch.js +33 -0
- package/src/risk.js +11 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ for (const l of r.lights) // the four traffic lights
|
|
|
28
28
|
| **Real concentration** | Largest accounts are traced to their owners. Pools and programs are separated from people, so `w1`/`w10` measure *people*, not AMM vaults. |
|
|
29
29
|
| **Inflated liquidity** | DexScreener values both sides of a pool at the pool's price. A pool with 4B of a token against 2 USDC shows "$4,000M liquidity". The engine recomputes it from the real side (USDC/USDT/SOL). It only flags extreme gaps (real side under 5%), because concentrated-liquidity pools are lopsided by nature. |
|
|
30
30
|
| **Unlocked LP** | % of LP locked/burned via RugCheck. It only counts for markets under 90 days old, so veteran tokens don't get false positives, and never while a token is on Pump.fun's bonding curve, where there is no LP to pull. |
|
|
31
|
-
| **Serial creators** | For markets under 7 days: how many other tokens the creator launched in the last 24 h (RugCheck). 2 or more is a warning, 5 or more a red flag. `recentLaunches(client, wallet)` counts the same on chain for the wallet that *paid* a launch, which catches farms that name a fresh creator each time. |
|
|
31
|
+
| **Serial creators** | For markets under 7 days: how many other tokens the creator launched in the last 24 h (RugCheck). 2 or more is a warning, 5 or more a red flag. `recentLaunches(client, wallet)` counts the same on chain for the wallet that *paid* a launch, which catches farms that name a fresh creator each time. When RugCheck has no report yet (it often answers 400 for tokens a few minutes old) the analysis falls back to `chainLaunches(client, mint)`: that count for the first signer of the token's creation transaction, reported as its own signal `CTL_SERIAL_PAYER` ("the wallet that paid for this launch…", never as the creator) and skipped when the SolFlag wallet base knows that wallet as an exchange or platform. |
|
|
32
32
|
| **Impostors** | Tokens using the symbol or name of a famous token (USDC, JUP, BONK, WIF, BTC, ETH...) with a different address. |
|
|
33
33
|
| **Real brands** | About 320 real companies with no token on Solana (Spotify, Nike, Zara, Ferrari...). A token called just that is a red flag and **HIGH**; the brand as one word of a longer name is a warning. Everyday words that are also brands (Shell, Target, Puma) are left out, and markets at least 30 days old with $100k+ liquidity are not checked. |
|
|
34
34
|
| **Known issuers and official tokens** | Stablecoins from Paxos (PYUSD, USDG), Circle (USDC, EURC) and Tether (USDT), tokenized shares from xStocks (all recognised by their control keys, taken from mints the issuer lists on its own pages) and official tokens from exchanges and Socios.com (bnSOL, bbSOL, cbBTC, club fan tokens..., by mint address) are shown as dependency on the issuer. Their large holders (treasury, custodians, exchanges) are shown but do not count as concentration, a 0% transfer fee is not a warning, and a tiny Solana pool is a warning, not a red flag. A short **trusted** list (Circle, Paxos, Tether, xStocks; `trustedIssuer()`) goes further: every light green, holders not checked. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
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/i18n/en.js
CHANGED
|
@@ -38,6 +38,7 @@ export default {
|
|
|
38
38
|
CTL_KNOWN_ISSUER: (d) => "Controlled by its issuer, " + d.issuer + " (see dependency)",
|
|
39
39
|
CTL_NONE: () => "No active authorities: nobody can mint more or freeze",
|
|
40
40
|
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",
|
|
41
|
+
CTL_SERIAL_PAYER: (d) => "The wallet that paid for this launch launched " + d.n + " other tokens in the last 24 h (counted on chain): a serial launcher, who usually drops each token as soon as buyers arrive",
|
|
41
42
|
BRAND_COPY: (d) => "Uses the name of " + d.brand + ", a real company with no token on Solana: it is not official, almost certainly an imitation to draw buyers",
|
|
42
43
|
BRAND_NAME: (d) => "Uses the name of " + d.brand + ", a real company: it is not an official " + d.brand + " token",
|
|
43
44
|
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",
|
package/src/i18n/es.js
CHANGED
|
@@ -1,120 +1,121 @@
|
|
|
1
|
-
// Spanish texts: the same wording the original single-file scanner used
|
|
2
|
-
function usd(n) {
|
|
3
|
-
if (!n) return "0 $";
|
|
4
|
-
if (n < 0.01) return "<0,01 $";
|
|
5
|
-
// de-DE instead of es-ES: same format (dot thousands, comma decimals), but es-ES does not group
|
|
6
|
-
// 4-digit numbers and "6494 $" showed next to "22.505 $"
|
|
7
|
-
return n.toLocaleString("de-DE", { minimumFractionDigits: n < 1000 ? 2 : 0, maximumFractionDigits: n < 1000 ? 2 : 0 }) + " $";
|
|
8
|
-
}
|
|
9
|
-
function pct(n) { return Math.round(n) + "%"; }
|
|
10
|
-
function short(a) { return a.slice(0, 4) + "…" + a.slice(-4); }
|
|
11
|
-
function prog(d) { return d && d.prog >= 20 ? ". Los pools y programas guardan el " + pct(d.prog) : ""; }
|
|
12
|
-
|
|
13
|
-
const EXT = {
|
|
14
|
-
permanentDelegate: "El emisor puede mover o quemar tus tokens (delegado permanente). Es normal en acciones tokenizadas, pero hay que confiar en el emisor",
|
|
15
|
-
transferHook: "Tiene transfer hook: ejecuta código externo en cada transferencia",
|
|
16
|
-
transferFeeConfig: "Cobra una comisión en cada transferencia",
|
|
17
|
-
nonTransferable: "No se puede transferir",
|
|
18
|
-
defaultAccountState: "Las cuentas nuevas pueden crearse congeladas"
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export default {
|
|
22
|
-
fmt: { usd: usd, pct: pct, short: short },
|
|
23
|
-
signals: {
|
|
24
|
-
NO_INFO: () => "No se pudo leer la información del token",
|
|
25
|
-
MINT_BY_PROGRAM: () => "El mint lo controla un programa (suele ser un puente o un protocolo): hay que confiar en ese código",
|
|
26
|
-
MINT_ACTIVE: (d) => "El creador todavía puede crear más tokens (mint activo" + (d && d.byWallet ? ", en manos de una wallet" : "") + ")",
|
|
27
|
-
FREEZE_BY_PROGRAM: () => "El freeze lo controla un programa: puede congelar cuentas según su código",
|
|
28
|
-
FREEZE_ACTIVE: (d) => "El creador puede congelar cuentas (freeze activo" + (d && d.byWallet ? ", en manos de una wallet" : "") + ")",
|
|
29
|
-
EXTENSION: (d) => EXT[d.ext] || ("Extensión " + d.ext),
|
|
30
|
-
NO_MARKET: () => "No hay mercado en DexScreener (sin precio ni liquidez)",
|
|
31
|
-
MARKET_UNCHECKED: () => "No se pudo consultar el mercado de este token (fallo de red al conectar con DexScreener): vuelve a analizarlo para comprobarlo",
|
|
32
|
-
KNOWN_TOKEN: () => "Token muy conocido",
|
|
33
|
-
|
|
34
|
-
CTL_MINT_ACTIVE: () => "El creador puede crear más tokens (mint activo)",
|
|
35
|
-
CTL_FREEZE_ACTIVE: () => "El creador puede congelar cuentas (freeze activo)",
|
|
36
|
-
CTL_TRANSFER_HOOK: () => "Ejecuta código externo en cada transferencia",
|
|
37
|
-
CTL_TRANSFER_FEE: () => "Cobra una comisión en cada transferencia",
|
|
38
|
-
CTL_NON_TRANSFERABLE: () => "No se puede transferir",
|
|
39
|
-
CTL_DEFAULT_FROZEN: () => "Las cuentas nuevas pueden crearse congeladas",
|
|
40
|
-
CTL_ONLY_PROGRAMS: () => "Solo hay autoridades controladas por un programa (ver dependencia)",
|
|
41
|
-
CTL_KNOWN_ISSUER: (d) => "Lo controla su emisor, " + d.issuer + " (ver dependencia)",
|
|
42
|
-
CTL_NONE: () => "Sin autoridades activas: nadie puede crear más ni congelar",
|
|
43
|
-
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",
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
1
|
+
// Spanish texts: the same wording the original single-file scanner used
|
|
2
|
+
function usd(n) {
|
|
3
|
+
if (!n) return "0 $";
|
|
4
|
+
if (n < 0.01) return "<0,01 $";
|
|
5
|
+
// de-DE instead of es-ES: same format (dot thousands, comma decimals), but es-ES does not group
|
|
6
|
+
// 4-digit numbers and "6494 $" showed next to "22.505 $"
|
|
7
|
+
return n.toLocaleString("de-DE", { minimumFractionDigits: n < 1000 ? 2 : 0, maximumFractionDigits: n < 1000 ? 2 : 0 }) + " $";
|
|
8
|
+
}
|
|
9
|
+
function pct(n) { return Math.round(n) + "%"; }
|
|
10
|
+
function short(a) { return a.slice(0, 4) + "…" + a.slice(-4); }
|
|
11
|
+
function prog(d) { return d && d.prog >= 20 ? ". Los pools y programas guardan el " + pct(d.prog) : ""; }
|
|
12
|
+
|
|
13
|
+
const EXT = {
|
|
14
|
+
permanentDelegate: "El emisor puede mover o quemar tus tokens (delegado permanente). Es normal en acciones tokenizadas, pero hay que confiar en el emisor",
|
|
15
|
+
transferHook: "Tiene transfer hook: ejecuta código externo en cada transferencia",
|
|
16
|
+
transferFeeConfig: "Cobra una comisión en cada transferencia",
|
|
17
|
+
nonTransferable: "No se puede transferir",
|
|
18
|
+
defaultAccountState: "Las cuentas nuevas pueden crearse congeladas"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
fmt: { usd: usd, pct: pct, short: short },
|
|
23
|
+
signals: {
|
|
24
|
+
NO_INFO: () => "No se pudo leer la información del token",
|
|
25
|
+
MINT_BY_PROGRAM: () => "El mint lo controla un programa (suele ser un puente o un protocolo): hay que confiar en ese código",
|
|
26
|
+
MINT_ACTIVE: (d) => "El creador todavía puede crear más tokens (mint activo" + (d && d.byWallet ? ", en manos de una wallet" : "") + ")",
|
|
27
|
+
FREEZE_BY_PROGRAM: () => "El freeze lo controla un programa: puede congelar cuentas según su código",
|
|
28
|
+
FREEZE_ACTIVE: (d) => "El creador puede congelar cuentas (freeze activo" + (d && d.byWallet ? ", en manos de una wallet" : "") + ")",
|
|
29
|
+
EXTENSION: (d) => EXT[d.ext] || ("Extensión " + d.ext),
|
|
30
|
+
NO_MARKET: () => "No hay mercado en DexScreener (sin precio ni liquidez)",
|
|
31
|
+
MARKET_UNCHECKED: () => "No se pudo consultar el mercado de este token (fallo de red al conectar con DexScreener): vuelve a analizarlo para comprobarlo",
|
|
32
|
+
KNOWN_TOKEN: () => "Token muy conocido",
|
|
33
|
+
|
|
34
|
+
CTL_MINT_ACTIVE: () => "El creador puede crear más tokens (mint activo)",
|
|
35
|
+
CTL_FREEZE_ACTIVE: () => "El creador puede congelar cuentas (freeze activo)",
|
|
36
|
+
CTL_TRANSFER_HOOK: () => "Ejecuta código externo en cada transferencia",
|
|
37
|
+
CTL_TRANSFER_FEE: () => "Cobra una comisión en cada transferencia",
|
|
38
|
+
CTL_NON_TRANSFERABLE: () => "No se puede transferir",
|
|
39
|
+
CTL_DEFAULT_FROZEN: () => "Las cuentas nuevas pueden crearse congeladas",
|
|
40
|
+
CTL_ONLY_PROGRAMS: () => "Solo hay autoridades controladas por un programa (ver dependencia)",
|
|
41
|
+
CTL_KNOWN_ISSUER: (d) => "Lo controla su emisor, " + d.issuer + " (ver dependencia)",
|
|
42
|
+
CTL_NONE: () => "Sin autoridades activas: nadie puede crear más ni congelar",
|
|
43
|
+
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",
|
|
44
|
+
CTL_SERIAL_PAYER: (d) => "La wallet que pagó este lanzamiento ha lanzado " + d.n + " tokens más en las últimas 24 h (contado en la cadena): es un lanzador en serie, que suele abandonar cada token en cuanto llegan compradores",
|
|
45
|
+
BRAND_COPY: (d) => "Usa el nombre de " + d.brand + ", una empresa real que no tiene token en Solana: no es oficial, casi seguro una imitación para atraer compradores",
|
|
46
|
+
BRAND_NAME: (d) => "Usa el nombre de " + d.brand + ", una empresa real: no es un token oficial de " + d.brand,
|
|
47
|
+
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",
|
|
48
|
+
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",
|
|
49
|
+
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",
|
|
50
|
+
IMPOSTOR: (d) => "No es el " + d.symbol + " original. Comprueba que es el token que buscas",
|
|
51
|
+
|
|
52
|
+
CON_SKIPPED: () => "Sin comprobar la concentración de holders",
|
|
53
|
+
CON_UNAVAILABLE: () => "No se pudo comprobar la concentración de holders (el servicio no respondió; prueba otra vez en un rato)",
|
|
54
|
+
CON_ALL_POOLS: (d) => "Casi todo el supply está en pools o programas" + prog(d),
|
|
55
|
+
CON_OK: (d) => "Sin concentración preocupante (mayor wallet individual: " + pct(d.w1) + ")" + prog(d),
|
|
56
|
+
CON_TRUSTED: (d) => "No cuenta: en un token de " + d.issuer + " los grandes holders son su tesorería, custodios, exchanges y protocolos",
|
|
57
|
+
CON_ISSUER: (d) => "La mayor wallet tiene el " + pct(d.w1) + " y las 10 mayores el " + pct(d.w10) + ": con un emisor verificado suelen ser su tesorería, custodios, exchanges o protocolos, no cuenta como riesgo" + prog(d),
|
|
58
|
+
CON_HIGH: (d) => "La mayor wallet individual tiene el " + pct(d.w1) + " y las 10 mayores el " + pct(d.w10) + " del supply" + prog(d),
|
|
59
|
+
CON_UNSPLIT: (d) => "No se pudo separar pools de personas. La mayor cuenta tiene el " + pct(d.top1) + " del supply",
|
|
60
|
+
|
|
61
|
+
MKT_LIQ_INFLATED: (d) => "Liquidez inflada: DexScreener dice " + usd(d.reported) + ", pero el pool solo tiene unos " + usd(d.real) + " en tokens de verdad (USDC, USDT o SOL). Es lo máximo que podrías sacar al vender" + (d.reported >= 100 * Math.max(d.real, 1) ? ". Una diferencia así no es casual: está montada para que el token parezca establecido" : ""),
|
|
62
|
+
MKT_LIQ_VERY_LOW: (d) => "Liquidez muy baja (" + usd(d.liq) + ")",
|
|
63
|
+
MKT_LIQ_LOW: (d) => "Liquidez baja (" + usd(d.liq) + ")",
|
|
64
|
+
MKT_VOLUME_LOW: (d) => "Volumen de 24 h casi nulo (" + usd(d.vol) + ")",
|
|
65
|
+
MKT_NEW: (d) => "Mercado muy nuevo (" + (d.days < 1 ? "menos de un día" : d.days + (d.days === 1 ? " día" : " días")) + ")",
|
|
66
|
+
MKT_POSITION_HEAVY: (d) => "La posición de esta wallet equivale al " + pct(d.pct) + " de la liquidez: vender de golpe movería mucho el precio",
|
|
67
|
+
MKT_LP_UNLOCKED: (d) => "Solo el " + pct(d.pct) + " de la liquidez está bloqueada o quemada (según RugCheck): el resto se puede retirar de golpe",
|
|
68
|
+
MKT_LP_PARTLY_LOCKED: (d) => "Solo el " + pct(d.pct) + " de la liquidez está bloqueada o quemada (según RugCheck)",
|
|
69
|
+
MKT_OK: (d) => "Liquidez " + usd(d.liq) + " · volumen 24 h " + usd(d.vol),
|
|
70
|
+
|
|
71
|
+
DEP_PROGRAM_AUTHORITY: () => "Un programa controla el mint o el freeze (puente o protocolo): hay que confiar en su código",
|
|
72
|
+
DEP_PERMANENT_DELEGATE: () => "El emisor puede mover o quemar tus tokens (delegado permanente): confías en el emisor",
|
|
73
|
+
DEP_STABLE_ISSUER: () => "Su valor depende de quien la emite y la respalda",
|
|
74
|
+
DEP_TRUSTED_ISSUER: (d) => "Lo emite " + d.issuer + ", un emisor regulado de los pocos que SolFlag considera de confianza. Como cualquier emisor, puede congelar o retirar tokens por orden legal",
|
|
75
|
+
DEP_KNOWN_ISSUER: (d) => "Lo emite " + d.issuer + ": puede crear, congelar o mover tokens, como cualquier emisor regulado. Confías en el emisor",
|
|
76
|
+
DEP_NONE: () => "No depende de un emisor ni de un puente"
|
|
77
|
+
},
|
|
78
|
+
offSolana: " y su mercado principal puede estar fuera de Solana",
|
|
79
|
+
levels: {
|
|
80
|
+
HIGH: "Riesgo alto", MEDIUM: "Riesgo medio", LOW: "Riesgo bajo", SPAM: "Probable spam",
|
|
81
|
+
NO_DATA: "Sin comprobar", NO_MARKET: "Sin mercado", MEDIUM_DATA_ONLY: "Medio por falta de datos"
|
|
82
|
+
},
|
|
83
|
+
types: {
|
|
84
|
+
STABLE: { label: "Stablecoin", note: "Se juzga por su liquidez y por la confianza en quien la emite; la concentración pesa menos." },
|
|
85
|
+
TOKENIZED: { label: "Tokenizado o regulado", note: "Lo normal es que el emisor tenga control (delegado permanente). El riesgo real es confiar en el emisor y que haya liquidez para salir." },
|
|
86
|
+
BRIDGED: { label: "Puenteado o de protocolo", note: "Lo normal es que un programa controle el mint. El riesgo real es confiar en ese puente y que su mercado en Solana sea pequeño, porque el principal puede estar en otra red." },
|
|
87
|
+
CREATOR: { label: "Con control del creador", note: "Una wallet puede crear más tokens o congelar cuentas: es el patrón típico de estafa, así que se juzga con más dureza." },
|
|
88
|
+
OPEN: { label: "Token abierto", note: "Sin autoridades del creador, típico de memecoins y proyectos nuevos. Pesan sobre todo la concentración en personas y la liquidez." }
|
|
89
|
+
},
|
|
90
|
+
lights: { control: "Control del creador", concentration: "Concentración", market: "Mercado y salida", dependency: "Dependencia" },
|
|
91
|
+
missing: { control: "quién controla el token", concentration: "la concentración de holders", market: "el mercado", dependency: "la dependencia de un emisor" },
|
|
92
|
+
steps: {
|
|
93
|
+
address: "Comprobando la dirección…", wallet: "Leyendo la wallet…", markets: "Consultando precios y liquidez…",
|
|
94
|
+
tokens: "Revisando tokens…", token: "Leyendo el token…", owners: "Buscando a quién pertenece cada cuenta…",
|
|
95
|
+
kinds: "Distinguiendo wallets de pools y programas…", bots: "Separando personas de bots y exchanges…",
|
|
96
|
+
funders: "Buscando wallets con el mismo origen de fondos…", exchangeSources: "Comprobando si el origen de cada grupo es un exchange…",
|
|
97
|
+
creator: "Mirando quién creó el token (RugCheck)…", creatorTokens: "Revisando otros tokens del creador…",
|
|
98
|
+
networks: "Comprobando cuánto tienen hoy las redes de wallets…", wallets: "Leyendo wallets…",
|
|
99
|
+
pnl: "Estimando el resultado de cada wallet…", done: "Listo"
|
|
100
|
+
},
|
|
101
|
+
notes: {
|
|
102
|
+
PNL_NO_HELIUS: "No se estimó el resultado de las wallets porque falta una clave de Helius.",
|
|
103
|
+
PNL_PARTIAL: "No se pudo leer el historial de algunas wallets.",
|
|
104
|
+
PNL_APPROX: "No se pudo obtener el histórico de precio de SOL: se usó el precio de hoy y los resultados pueden estar muy alejados de la realidad."
|
|
105
|
+
},
|
|
106
|
+
errors: {
|
|
107
|
+
CANCELLED: () => "Cancelado.",
|
|
108
|
+
INVALID_ADDRESS: () => "Esa dirección no parece de Solana. Son de 32 a 44 letras y números y no empiezan por 0x.",
|
|
109
|
+
NOT_A_TOKEN: () => "Esa dirección no es un token.",
|
|
110
|
+
IS_NFT: () => "Esa dirección es un NFT, no un token. El escáner no revisa NFTs.",
|
|
111
|
+
NO_HOLDERS: () => "El RPC no devolvió datos de holders.",
|
|
112
|
+
HELIUS_REQUIRED: () => "Para estimar el resultado hace falta una clave de Helius (o una URL de RPC de Helius con la clave).",
|
|
113
|
+
SOL_PRICE: () => "No se pudo obtener el precio de SOL para calcular el resultado. Prueba otra vez dentro de un rato.",
|
|
114
|
+
DEX_UNREACHABLE: () => "No se pudo conectar con DexScreener. Revisa tu conexión a internet y prueba otra vez.",
|
|
115
|
+
TOO_FEW_WALLETS: (d) => d && d.filter ? "Solo " + d.n + (d.n === 1 ? " wallet cumple" : " wallets cumplen") + " el filtro de resultado (de " + d.total + " leídas)." : "Hacen falta al menos 2 wallets que se puedan leer.",
|
|
116
|
+
TOO_FEW_TOKENS: () => "Pega al menos 2 direcciones de tokens de Solana.",
|
|
117
|
+
NETWORK_PUBLIC: () => "Los servicios gratuitos de Solana no han respondido (a veces se saturan). Espera un minuto y prueba otra vez, o usa tu propia URL de RPC (Helius tiene plan gratuito).",
|
|
118
|
+
NETWORK_OWN: () => "No se pudo conectar con tu URL de RPC. Revisa que esté bien copiada (con la clave al final).",
|
|
119
|
+
UNKNOWN: (d) => "Algo ha fallado: " + d.message + ". Prueba otra vez o usa otro RPC."
|
|
120
|
+
}
|
|
121
|
+
};
|
package/src/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf
|
|
|
49
49
|
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair, brandOf, copiesOf, fakeLiquidity, FAKE_LIQ_RATIO } from "./markets.js";
|
|
50
50
|
export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches, CREATOR_DEAD_MCAP } from "./rugcheck.js";
|
|
51
51
|
export { score, issuerOf, trustedIssuer, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
|
|
52
|
-
export { recentLaunches } from "./launch.js";
|
|
52
|
+
export { recentLaunches, chainLaunches } from "./launch.js";
|
|
53
53
|
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
54
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";
|
package/src/launch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PUMP_PROGRAM } from "./constants.js";
|
|
2
2
|
import { isCancel } from "./errors.js";
|
|
3
3
|
import { pool } from "./util.js";
|
|
4
|
+
import { loadWalletBase, knownType } from "./base.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Pump.fun tokens a wallet launched in the last 24 h, read on chain from its latest transactions.
|
|
@@ -42,3 +43,35 @@ export async function recentLaunches(client, wallet, opts) {
|
|
|
42
43
|
});
|
|
43
44
|
return { n: mints.length, mints: mints, scanned: day.length, more: sigs.length >= limit && day.length === sigs.length };
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The creator's recent launches read on chain, for when RugCheck does not know the token yet (it answers
|
|
49
|
+
* 400 "unable to generate report" for most tokens a few minutes old, 2026-11-02) or does not answer.
|
|
50
|
+
* The launcher is the first signer of the token's creation transaction: the wallet that paid, as in
|
|
51
|
+
* recentLaunches (farms name a fresh creator each time but pay from one wallet). Only new tokens: past
|
|
52
|
+
* 1000 transactions the creation is too far back to find cheaply.
|
|
53
|
+
* The wallet is NOT the creator RugCheck names (farms name a fresh one each time), so it comes back as
|
|
54
|
+
* `payer` with `creator: null`, and the signal is CTL_SERIAL_PAYER with its own text. A wallet the
|
|
55
|
+
* SolFlag wallet base knows as an exchange or platform (it may pay launches for many unrelated people)
|
|
56
|
+
* is not counted; opts.base: a base object, or false to skip the check.
|
|
57
|
+
* Same shape as creatorLaunches, without the history of older launches (that needs market caps):
|
|
58
|
+
* @returns {creator: null, payer, day, total, old: 0, dead: 0, best: null, chain: true, more} or null
|
|
59
|
+
*/
|
|
60
|
+
export async function chainLaunches(client, mint, opts) {
|
|
61
|
+
opts = opts || {};
|
|
62
|
+
const sigs = (await client.rpc("getSignaturesForAddress", [mint, { limit: 1000 }])) || [];
|
|
63
|
+
if (!sigs.length || sigs.length >= 1000) return null;
|
|
64
|
+
const p = { encoding: "jsonParsed", maxSupportedTransactionVersion: 1, commitment: "confirmed" };
|
|
65
|
+
const tx = await client.rpc("getTransaction", [sigs[sigs.length - 1].signature, p]);
|
|
66
|
+
const keys = tx && tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys;
|
|
67
|
+
const first = keys && keys.filter(function (k) { return k.signer; })[0];
|
|
68
|
+
const wallet = first && (first.pubkey || first);
|
|
69
|
+
if (!wallet || wallet === mint) return null;
|
|
70
|
+
if (opts.base !== false) {
|
|
71
|
+
const base = opts.base && typeof opts.base === "object" ? opts.base : await loadWalletBase(client);
|
|
72
|
+
if (knownType(base, wallet) === "exchange") return null;
|
|
73
|
+
}
|
|
74
|
+
const r = await recentLaunches(client, wallet, { now: opts.now, limit: opts.limit });
|
|
75
|
+
const day = r.mints.filter(function (m) { return m !== mint; }).length;
|
|
76
|
+
return { creator: null, payer: wallet, day: day, total: day, old: 0, dead: 0, best: null, chain: true, more: r.more };
|
|
77
|
+
}
|
package/src/risk.js
CHANGED
|
@@ -4,6 +4,7 @@ import { short, sleep } from "./util.js";
|
|
|
4
4
|
import { getMint, getAuthKinds, getConc } from "./tokens.js";
|
|
5
5
|
import { impostorOf, brandOf, copiesOf, fakeLiquidity } from "./markets.js";
|
|
6
6
|
import { rugcheckSummary, creatorLaunches } from "./rugcheck.js";
|
|
7
|
+
import { chainLaunches } from "./launch.js";
|
|
7
8
|
|
|
8
9
|
// Every finding is a "signal": {code, s, data}. `s` is ok | na | warn | bad, `data` holds the numbers
|
|
9
10
|
// behind it. Turning signals into sentences is i18n's job (describe / explainLight), never the engine's.
|
|
@@ -131,7 +132,12 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
|
|
|
131
132
|
}
|
|
132
133
|
// Launch farms put out a token every few minutes and drop each one as soon as buyers arrive: in a
|
|
133
134
|
// 100-token Pump.fun sample one wallet launched 7 copies of "GMS" in 10 minutes
|
|
134
|
-
|
|
135
|
+
// counted on chain (RugCheck had no report): it is the wallet that paid the launch, not the named creator
|
|
136
|
+
if (launches && launches.day >= SERIAL_WARN) {
|
|
137
|
+
const lv = launches.day >= SERIAL_BAD ? "bad" : "warn";
|
|
138
|
+
it.push(launches.chain ? sig("CTL_SERIAL_PAYER", lv, { n: launches.day, payer: launches.payer })
|
|
139
|
+
: sig("CTL_SERIAL_CREATOR", lv, { n: launches.day, creator: launches.creator }));
|
|
140
|
+
}
|
|
135
141
|
// Older launches that went nowhere: farms that spread their tokens over days escape the 24 h count
|
|
136
142
|
if (launches && launches.old >= HISTORY_MIN && launches.dead / launches.old >= HISTORY_WARN) {
|
|
137
143
|
const bad = launches.old >= HISTORY_BAD_MIN && launches.dead / launches.old >= HISTORY_BAD;
|
|
@@ -264,7 +270,10 @@ export async function assess(client, mint, pair, opts) {
|
|
|
264
270
|
const rcPromise = pair ? rugcheckSummary(client, mint) : Promise.resolve(null); // in parallel with the RPC calls below
|
|
265
271
|
// Serial launching only matters while a token is new; RugCheck's full report is small (~10 KB) then
|
|
266
272
|
const fresh = pair && !opts.skipCreator && (!pair.pairCreatedAt || Date.now() - pair.pairCreatedAt < 7 * DAY);
|
|
267
|
-
|
|
273
|
+
// when RugCheck does not know the creator (new tokens often get a 400) the launches are counted on chain
|
|
274
|
+
const launchPromise = fresh ? creatorLaunches(client, mint).then(function (r) {
|
|
275
|
+
return r || chainLaunches(client, mint).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
276
|
+
}) : Promise.resolve(null);
|
|
268
277
|
// Copies of the same name and ticker: one search per token, so only when asked (a single token, not a wallet)
|
|
269
278
|
const copiesPromise = pair && opts.copies && !OFFICIAL_TOKENS[mint] ? copiesOf(client, mint, pair).catch(function (e) { if (isCancel(e)) throw e; return null; }) : Promise.resolve(null);
|
|
270
279
|
let info = null, conc = null;
|