solana-risk-engine 0.3.0 → 0.3.1
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/constants.js +1 -0
- package/src/index.js +1 -1
- package/src/markets.js +30 -1
- package/src/wallet.js +17 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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/constants.js
CHANGED
|
@@ -6,6 +6,7 @@ export const PUBLIC_RPC = "https://solana-rpc.publicnode.com";
|
|
|
6
6
|
export const VIBE_RPC = "https://public.rpc.solanavibestation.com";
|
|
7
7
|
export const DEX = "https://api.dexscreener.com/tokens/v1/solana/";
|
|
8
8
|
export const RUGCHECK = "https://api.rugcheck.xyz/v1/tokens/";
|
|
9
|
+
export const GECKO = "https://api.geckoterminal.com/api/v2/networks/solana/";
|
|
9
10
|
export const HELIUS_API = "https://api.helius.xyz/v0/addresses/";
|
|
10
11
|
export const COINGECKO_SOL = "https://api.coingecko.com/api/v3/coins/solana/market_chart?vs_currency=usd&days=365";
|
|
11
12
|
|
package/src/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export { ScanError, isCancel } from "./errors.js";
|
|
|
44
44
|
export * from "./constants.js";
|
|
45
45
|
export { isOnCurve, onCurve, b58bytes } from "./curve.js";
|
|
46
46
|
export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf } from "./tokens.js";
|
|
47
|
-
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO } from "./markets.js";
|
|
47
|
+
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair } from "./markets.js";
|
|
48
48
|
export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches } from "./rugcheck.js";
|
|
49
49
|
export { score, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD } from "./risk.js";
|
|
50
50
|
export { recentLaunches } from "./launch.js";
|
package/src/markets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEX, USDC, USDT, WSOL, FAMOUS } from "./constants.js";
|
|
1
|
+
import { DEX, GECKO, USDC, USDT, WSOL, FAMOUS } from "./constants.js";
|
|
2
2
|
import { ScanError, isCancel } from "./errors.js";
|
|
3
3
|
import { sleep } from "./util.js";
|
|
4
4
|
|
|
@@ -56,3 +56,32 @@ export async function getMarkets(client, mints, onStep) {
|
|
|
56
56
|
}
|
|
57
57
|
return { data: best, failed: failed };
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* ---------- GeckoTerminal: fills two DexScreener gaps on Pump.fun ---------- */
|
|
61
|
+
// Both are optional extras: on any failure the market stays exactly as DexScreener gave it.
|
|
62
|
+
|
|
63
|
+
// DexScreener reports $0 liquidity for tokens still on Pump.fun's bonding curve. GeckoTerminal has the
|
|
64
|
+
// real figure for the same pool (its pool address is DexScreener's pairAddress). Mutates the pair.
|
|
65
|
+
export async function geckoLiquidity(client, pair) {
|
|
66
|
+
if (!pair || (pair.liquidity && pair.liquidity.usd) || !pair.pairAddress) return pair;
|
|
67
|
+
const j = await client.softJson(GECKO + "pools/" + pair.pairAddress, 6000);
|
|
68
|
+
const v = Number(j && j.data && j.data.attributes && j.data.attributes.reserve_in_usd) || 0;
|
|
69
|
+
if (v > 0) pair.liquidity = Object.assign({}, pair.liquidity, { usd: v });
|
|
70
|
+
return pair;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A token seconds old is not on DexScreener yet but is on GeckoTerminal: its most liquid pool, shaped
|
|
74
|
+
// like a DexScreener pair (what the engine reads: price, liquidity, volume, age, symbol), or null
|
|
75
|
+
export async function geckoPair(client, mint) {
|
|
76
|
+
const j = await client.softJson(GECKO + "tokens/" + mint + "/pools?page=1&include=base_token", 6000);
|
|
77
|
+
const p = j && j.data && j.data[0];
|
|
78
|
+
if (!p || !p.attributes) return null;
|
|
79
|
+
const a = p.attributes, rel = p.relationships || {}, t = ((j.included || [])[0] || {}).attributes || {};
|
|
80
|
+
return {
|
|
81
|
+
pairAddress: a.address, dexId: (rel.dex && rel.dex.data && rel.dex.data.id) || "",
|
|
82
|
+
baseToken: { address: mint, symbol: t.symbol, name: t.name },
|
|
83
|
+
priceUsd: String(a.base_token_price_usd || 0), liquidity: { usd: Number(a.reserve_in_usd) || 0 },
|
|
84
|
+
volume: { h24: Number(a.volume_usd && a.volume_usd.h24) || 0 }, pairCreatedAt: Date.parse(a.pool_created_at) || 0,
|
|
85
|
+
info: { imageUrl: t.image_url && !/missing/.test(t.image_url) ? t.image_url : null }, source: "geckoterminal"
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/wallet.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ADDRESS_RE, KNOWN, LEVELS } from "./constants.js";
|
|
1
|
+
import { ADDRESS_RE, KNOWN, LEVELS, PUMP_CURVE_DEXES } from "./constants.js";
|
|
2
2
|
import { ScanError } from "./errors.js";
|
|
3
3
|
import { pool, progress } from "./util.js";
|
|
4
4
|
import { getTokens, isMint } from "./tokens.js";
|
|
5
|
-
import { getMarkets } from "./markets.js";
|
|
5
|
+
import { getMarkets, geckoLiquidity, geckoPair } from "./markets.js";
|
|
6
6
|
import { assess } from "./risk.js";
|
|
7
7
|
|
|
8
8
|
function checkAddress(addr) {
|
|
@@ -15,12 +15,20 @@ export function sortResults(list) {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
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
|
+
*/
|
|
19
22
|
export async function analyzeToken(client, mint, opts) {
|
|
20
23
|
opts = opts || {};
|
|
21
24
|
checkAddress(mint);
|
|
22
25
|
progress(opts.onProgress, "markets", 0.15);
|
|
23
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
|
+
}
|
|
24
32
|
progress(opts.onProgress, "token", 0.45);
|
|
25
33
|
const r = await assess(client, mint, mkr.data[mint] || null, { marketUnknown: mkr.failed.length > 0, skipConcentration: opts.skipConcentration, skipCreator: opts.skipCreator });
|
|
26
34
|
if (!r) throw new ScanError("IS_NFT", "that address is an NFT, not a token");
|
|
@@ -49,6 +57,12 @@ export async function analyzeWallet(client, wallet, opts) {
|
|
|
49
57
|
const mkr = await getMarkets(client, mints, function (d, n) { progress(onProgress, "markets", 0.08 + 0.12 * d / n, { done: d, total: n }); });
|
|
50
58
|
const mk = mkr.data, failedMk = {};
|
|
51
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
|
+
}
|
|
52
66
|
|
|
53
67
|
const out = [], todo = [];
|
|
54
68
|
mints.forEach(function (m) {
|