solana-risk-engine 0.4.2 → 0.4.3
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 +144 -144
- package/package.json +47 -47
- package/src/client.js +157 -143
- package/src/constants.js +78 -62
- package/src/holders.js +260 -259
- package/src/i18n/en.js +112 -109
- package/src/i18n/es.js +115 -112
- package/src/index.js +57 -57
- package/src/markets.js +136 -87
- package/src/risk.js +268 -255
- package/src/wallet.js +94 -94
package/src/index.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import { createClient } from "./client.js";
|
|
2
|
-
import { analyzeToken, analyzeWallet, analyzeAddress } from "./wallet.js";
|
|
3
|
-
import { analyzeHolders } from "./holders.js";
|
|
4
|
-
import { estimatePnl } from "./pnl.js";
|
|
5
|
-
import { takeSnapshots } from "./changes.js";
|
|
6
|
-
import { confluence, commonHolders } from "./confluence.js";
|
|
7
|
-
import { getTokens } from "./tokens.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The easy entry point: one object with every high-level analysis bound to one network client.
|
|
11
|
-
*
|
|
12
|
-
* const scanner = createScanner({ rpcUrl: "https://mainnet.helius-rpc.com/?api-key=..." });
|
|
13
|
-
* const r = await scanner.analyzeToken(mint);
|
|
14
|
-
*
|
|
15
|
-
* Options are those of createClient (rpcUrl, heliusApiKey, signal, fetch...). Each method also
|
|
16
|
-
* accepts its own opts (onProgress, ...), see the module docs.
|
|
17
|
-
*/
|
|
18
|
-
export function createScanner(opts) {
|
|
19
|
-
const client = createClient(opts);
|
|
20
|
-
return {
|
|
21
|
-
client: client,
|
|
22
|
-
analyzeToken: (mint, o) => analyzeToken(client, mint, o),
|
|
23
|
-
analyzeWallet: (wallet, o) => analyzeWallet(client, wallet, o),
|
|
24
|
-
analyzeAddress: (addr, o) => analyzeAddress(client, addr, o),
|
|
25
|
-
analyzeHolders: (mint, o) => analyzeHolders(client, mint, o),
|
|
26
|
-
estimatePnl: async (wallets, o) => {
|
|
27
|
-
// Current balances trim tokens the wallet moved away; read them if the caller did not pass them
|
|
28
|
-
o = o || {};
|
|
29
|
-
let bal = o.balances;
|
|
30
|
-
if (!bal) {
|
|
31
|
-
bal = {};
|
|
32
|
-
for (const w of wallets) bal[w] = (await getTokens(client, w)).tokens;
|
|
33
|
-
}
|
|
34
|
-
return estimatePnl(client, wallets, bal, o);
|
|
35
|
-
},
|
|
36
|
-
takeSnapshots: (wallets, o) => takeSnapshots(client, wallets, o),
|
|
37
|
-
confluence: (wallets, o) => confluence(client, wallets, o),
|
|
38
|
-
commonHolders: (mints, o) => commonHolders(client, mints, o)
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export { createClient };
|
|
43
|
-
export { ScanError, isCancel } from "./errors.js";
|
|
44
|
-
export * from "./constants.js";
|
|
45
|
-
export { isOnCurve, onCurve, b58bytes } from "./curve.js";
|
|
46
|
-
export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf } from "./tokens.js";
|
|
47
|
-
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair } from "./markets.js";
|
|
48
|
-
export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches, CREATOR_DEAD_MCAP } from "./rugcheck.js";
|
|
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
|
-
export { recentLaunches } from "./launch.js";
|
|
51
|
-
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
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";
|
|
54
|
-
export { fetchSwaps, solHistory, priceSeries, parseTrades, pnlFromTrades, pnlStatus, estimatePnl } from "./pnl.js";
|
|
55
|
-
export { capSnap, chgDiff, chgAgg, takeSnapshots, diffSnapshots } from "./changes.js";
|
|
56
|
-
export { confluence, commonHolders } from "./confluence.js";
|
|
57
|
-
export { describe, explainLight, lightLabel, levelLabel, missingText, typeInfo, describeStep, describeNote, describeError, format, addLanguage } from "./i18n/index.js";
|
|
1
|
+
import { createClient } from "./client.js";
|
|
2
|
+
import { analyzeToken, analyzeWallet, analyzeAddress } from "./wallet.js";
|
|
3
|
+
import { analyzeHolders } from "./holders.js";
|
|
4
|
+
import { estimatePnl } from "./pnl.js";
|
|
5
|
+
import { takeSnapshots } from "./changes.js";
|
|
6
|
+
import { confluence, commonHolders } from "./confluence.js";
|
|
7
|
+
import { getTokens } from "./tokens.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The easy entry point: one object with every high-level analysis bound to one network client.
|
|
11
|
+
*
|
|
12
|
+
* const scanner = createScanner({ rpcUrl: "https://mainnet.helius-rpc.com/?api-key=..." });
|
|
13
|
+
* const r = await scanner.analyzeToken(mint);
|
|
14
|
+
*
|
|
15
|
+
* Options are those of createClient (rpcUrl, heliusApiKey, signal, fetch...). Each method also
|
|
16
|
+
* accepts its own opts (onProgress, ...), see the module docs.
|
|
17
|
+
*/
|
|
18
|
+
export function createScanner(opts) {
|
|
19
|
+
const client = createClient(opts);
|
|
20
|
+
return {
|
|
21
|
+
client: client,
|
|
22
|
+
analyzeToken: (mint, o) => analyzeToken(client, mint, o),
|
|
23
|
+
analyzeWallet: (wallet, o) => analyzeWallet(client, wallet, o),
|
|
24
|
+
analyzeAddress: (addr, o) => analyzeAddress(client, addr, o),
|
|
25
|
+
analyzeHolders: (mint, o) => analyzeHolders(client, mint, o),
|
|
26
|
+
estimatePnl: async (wallets, o) => {
|
|
27
|
+
// Current balances trim tokens the wallet moved away; read them if the caller did not pass them
|
|
28
|
+
o = o || {};
|
|
29
|
+
let bal = o.balances;
|
|
30
|
+
if (!bal) {
|
|
31
|
+
bal = {};
|
|
32
|
+
for (const w of wallets) bal[w] = (await getTokens(client, w)).tokens;
|
|
33
|
+
}
|
|
34
|
+
return estimatePnl(client, wallets, bal, o);
|
|
35
|
+
},
|
|
36
|
+
takeSnapshots: (wallets, o) => takeSnapshots(client, wallets, o),
|
|
37
|
+
confluence: (wallets, o) => confluence(client, wallets, o),
|
|
38
|
+
commonHolders: (mints, o) => commonHolders(client, mints, o)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { createClient };
|
|
43
|
+
export { ScanError, isCancel } from "./errors.js";
|
|
44
|
+
export * from "./constants.js";
|
|
45
|
+
export { isOnCurve, onCurve, b58bytes } from "./curve.js";
|
|
46
|
+
export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf } from "./tokens.js";
|
|
47
|
+
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair, brandOf, copiesOf, fakeLiquidity, FAKE_LIQ_RATIO } from "./markets.js";
|
|
48
|
+
export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches, CREATOR_DEAD_MCAP } from "./rugcheck.js";
|
|
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
|
+
export { recentLaunches } from "./launch.js";
|
|
51
|
+
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
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";
|
|
54
|
+
export { fetchSwaps, solHistory, priceSeries, parseTrades, pnlFromTrades, pnlStatus, estimatePnl } from "./pnl.js";
|
|
55
|
+
export { capSnap, chgDiff, chgAgg, takeSnapshots, diffSnapshots } from "./changes.js";
|
|
56
|
+
export { confluence, commonHolders } from "./confluence.js";
|
|
57
|
+
export { describe, explainLight, lightLabel, levelLabel, missingText, typeInfo, describeStep, describeNote, describeError, format, addLanguage } from "./i18n/index.js";
|
package/src/markets.js
CHANGED
|
@@ -1,87 +1,136 @@
|
|
|
1
|
-
import { DEX, GECKO, USDC, USDT, WSOL, FAMOUS } from "./constants.js";
|
|
2
|
-
import { ScanError, isCancel } from "./errors.js";
|
|
3
|
-
import { sleep } from "./util.js";
|
|
4
|
-
|
|
5
|
-
// DexScreener values both sides of a pool at the pool's own price. A scammer creates a pool with
|
|
6
|
-
// 4 billion of their token against 2 real USDC and it shows "liquidity $4,000M". What you can actually
|
|
7
|
-
// take out when selling is the real-token side (USDC, USDT or SOL): if that is much smaller, it wins.
|
|
8
|
-
// Concentrated-liquidity pools (Orca Whirlpools, Raydium CLMM, Meteora DLMM) are lopsided by nature,
|
|
9
|
-
// often 10-20% real side, so only an extreme gap counts: real side under 5% of the reported value.
|
|
10
|
-
// The scams this targets sit far below that (2 USDC against "$4,000M" is 0.00000005%).
|
|
11
|
-
// Mutates the pair: keeps the reported figure in liqRaw and replaces liquidity.usd with the real one.
|
|
12
|
-
export const INFLATED_RATIO = 0.05;
|
|
13
|
-
|
|
14
|
-
export function realLiq(p) {
|
|
15
|
-
const l = p.liquidity, qa = p.quoteToken && p.quoteToken.address;
|
|
16
|
-
if (!l || !l.usd || (qa !== USDC && qa !== USDT && qa !== WSOL)) return;
|
|
17
|
-
const pn = Number(p.priceNative), pu = Number(p.priceUsd), q = Number(l.quote);
|
|
18
|
-
if (!(pn > 0 && pu > 0 && q >= 0)) return;
|
|
19
|
-
const real = 2 * q * pu / pn; // pu/pn = USD price of the real token
|
|
20
|
-
if (real < INFLATED_RATIO * l.usd) { p.liqRaw = l.usd; p.liquidity = Object.assign({}, l, { usd: real }); }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// A token using the symbol (or name) of a famous one but with another address is a copy
|
|
24
|
-
export function impostorOf(mint, pair) {
|
|
25
|
-
const b = (pair && pair.baseToken) || {};
|
|
26
|
-
let s = String(b.symbol || "").replace(/[\s$]/g, "").toUpperCase();
|
|
27
|
-
const n = String(b.name || "").trim().toLowerCase();
|
|
28
|
-
if (!FAMOUS[s]) { if (n === "usd coin") s = "USDC"; else if (n === "tether usd" || n === "tether") s = "USDT"; }
|
|
29
|
-
return (FAMOUS[s] && FAMOUS[s] !== mint) ? { sym: s, real: FAMOUS[s] } : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
1
|
+
import { DEX, DEX_SEARCH, GECKO, USDC, USDT, WSOL, FAMOUS, BRANDS } from "./constants.js";
|
|
2
|
+
import { ScanError, isCancel } from "./errors.js";
|
|
3
|
+
import { sleep } from "./util.js";
|
|
4
|
+
|
|
5
|
+
// DexScreener values both sides of a pool at the pool's own price. A scammer creates a pool with
|
|
6
|
+
// 4 billion of their token against 2 real USDC and it shows "liquidity $4,000M". What you can actually
|
|
7
|
+
// take out when selling is the real-token side (USDC, USDT or SOL): if that is much smaller, it wins.
|
|
8
|
+
// Concentrated-liquidity pools (Orca Whirlpools, Raydium CLMM, Meteora DLMM) are lopsided by nature,
|
|
9
|
+
// often 10-20% real side, so only an extreme gap counts: real side under 5% of the reported value.
|
|
10
|
+
// The scams this targets sit far below that (2 USDC against "$4,000M" is 0.00000005%).
|
|
11
|
+
// Mutates the pair: keeps the reported figure in liqRaw and replaces liquidity.usd with the real one.
|
|
12
|
+
export const INFLATED_RATIO = 0.05;
|
|
13
|
+
|
|
14
|
+
export function realLiq(p) {
|
|
15
|
+
const l = p.liquidity, qa = p.quoteToken && p.quoteToken.address;
|
|
16
|
+
if (!l || !l.usd || (qa !== USDC && qa !== USDT && qa !== WSOL)) return;
|
|
17
|
+
const pn = Number(p.priceNative), pu = Number(p.priceUsd), q = Number(l.quote);
|
|
18
|
+
if (!(pn > 0 && pu > 0 && q >= 0)) return;
|
|
19
|
+
const real = 2 * q * pu / pn; // pu/pn = USD price of the real token
|
|
20
|
+
if (real < INFLATED_RATIO * l.usd) { p.liqRaw = l.usd; p.liquidity = Object.assign({}, l, { usd: real }); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A token using the symbol (or name) of a famous one but with another address is a copy
|
|
24
|
+
export function impostorOf(mint, pair) {
|
|
25
|
+
const b = (pair && pair.baseToken) || {};
|
|
26
|
+
let s = String(b.symbol || "").replace(/[\s$]/g, "").toUpperCase();
|
|
27
|
+
const n = String(b.name || "").trim().toLowerCase();
|
|
28
|
+
if (!FAMOUS[s]) { if (n === "usd coin") s = "USDC"; else if (n === "tether usd" || n === "tether") s = "USDT"; }
|
|
29
|
+
return (FAMOUS[s] && FAMOUS[s] !== mint) ? { sym: s, real: FAMOUS[s] } : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Liquidity reported at 100 times what is really there is not a quirk of the pool: it is set up so the
|
|
33
|
+
// token looks established (a "Shopify" showing $12.9M with $52 behind it)
|
|
34
|
+
export const FAKE_LIQ_RATIO = 100;
|
|
35
|
+
export function fakeLiquidity(pair) {
|
|
36
|
+
const real = (pair && pair.liquidity && pair.liquidity.usd) || 0;
|
|
37
|
+
return !!(pair && pair.liqRaw && pair.liqRaw >= FAKE_LIQ_RATIO * Math.max(real, 1));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A real company's name on a token that is not issued by anyone: {brand, exact}. exact = the name or the
|
|
41
|
+
// symbol is just the brand ("Spotify"); otherwise the brand is one word of a longer name ("Spotify Cat")
|
|
42
|
+
export function brandOf(pair, type) {
|
|
43
|
+
if (type === "TOKENIZED" || type === "BRIDGED" || type === "STABLE") return null;
|
|
44
|
+
const b = (pair && pair.baseToken) || {};
|
|
45
|
+
const clean = function (s) { return String(s || "").toLowerCase().replace(/[$'’.\-_]/g, " ").replace(/\s+/g, " ").trim(); };
|
|
46
|
+
const name = clean(b.name), sym = clean(b.symbol);
|
|
47
|
+
for (const k of Object.keys(BRANDS)) {
|
|
48
|
+
const w = k.replace(/_/g, " ");
|
|
49
|
+
if (name === w || sym === w || name.replace(/ /g, "") === w.replace(/ /g, "")) return { brand: BRANDS[k], exact: true };
|
|
50
|
+
if ((" " + name + " ").indexOf(" " + w + " ") >= 0) return { brand: BRANDS[k], exact: false };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Other Solana tokens with the same name and symbol (DexScreener search). The oldest one is taken as
|
|
56
|
+
// the original. Returns null when this token is the oldest or nothing else matches.
|
|
57
|
+
export async function copiesOf(client, mint, pair, now) {
|
|
58
|
+
const b = (pair && pair.baseToken) || {};
|
|
59
|
+
if (!b.symbol) return null;
|
|
60
|
+
const res = await client.softJson(DEX_SEARCH + encodeURIComponent(b.symbol), 8000);
|
|
61
|
+
const norm = function (s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]/g, ""); };
|
|
62
|
+
const sym = norm(b.symbol), name = norm(b.name);
|
|
63
|
+
const by = {};
|
|
64
|
+
((res && res.pairs) || []).forEach(function (p) {
|
|
65
|
+
const t = p.baseToken || {};
|
|
66
|
+
if (p.chainId !== "solana" || !t.address || norm(t.symbol) !== sym || norm(t.name) !== name) return;
|
|
67
|
+
const cur = by[t.address];
|
|
68
|
+
const liq = (p.liquidity && p.liquidity.usd) || 0, at = p.pairCreatedAt || 0;
|
|
69
|
+
if (!cur) by[t.address] = { addr: t.address, liq: liq, at: at };
|
|
70
|
+
else { cur.liq = Math.max(cur.liq, liq); if (at && (!cur.at || at < cur.at)) cur.at = at; }
|
|
71
|
+
});
|
|
72
|
+
const mine = by[mint] || { addr: mint, liq: (pair.liquidity && pair.liquidity.usd) || 0, at: pair.pairCreatedAt || 0 };
|
|
73
|
+
const others = Object.keys(by).filter(function (a) { return a !== mint; }).map(function (a) { return by[a]; });
|
|
74
|
+
if (!others.length) return null;
|
|
75
|
+
const oldest = others.filter(function (o) { return o.at; }).sort(function (x, y) { return x.at - y.at; })[0];
|
|
76
|
+
// this token is itself the oldest (or the dates are unknown): it is the original, the others copy it
|
|
77
|
+
if (!oldest || (mine.at && mine.at <= oldest.at)) return null;
|
|
78
|
+
return { n: others.length, original: oldest.addr, days: Math.floor(((now || Date.now()) - oldest.at) / 86400000), origLiq: oldest.liq, liq: mine.liq };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Best (most liquid) DexScreener pair per mint, 30 mints per request.
|
|
82
|
+
// `failed` lists mints whose batch failed: unknown, which is not the same as "no market".
|
|
83
|
+
export async function getMarkets(client, mints, onStep) {
|
|
84
|
+
const best = {};
|
|
85
|
+
let failed = [];
|
|
86
|
+
for (let i = 0; i < mints.length; i += 30) {
|
|
87
|
+
const lote = mints.slice(i, i + 30);
|
|
88
|
+
try {
|
|
89
|
+
const pares = await client.jfetch(DEX + lote.join(","));
|
|
90
|
+
if (Array.isArray(pares)) pares.forEach(function (p) {
|
|
91
|
+
const m = p.baseToken && p.baseToken.address;
|
|
92
|
+
if (lote.indexOf(m) < 0) return;
|
|
93
|
+
realLiq(p);
|
|
94
|
+
const liq = (p.liquidity && p.liquidity.usd) || 0;
|
|
95
|
+
const cur = best[m] && best[m].liquidity && best[m].liquidity.usd || 0;
|
|
96
|
+
if (!best[m] || liq > cur) best[m] = p;
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (isCancel(e)) throw e;
|
|
100
|
+
if (i === 0 && e.code === "NETWORK") throw new ScanError("DEX_UNREACHABLE", "could not reach DexScreener");
|
|
101
|
+
failed = failed.concat(lote);
|
|
102
|
+
}
|
|
103
|
+
if (onStep) onStep(Math.min(i + 30, mints.length), mints.length);
|
|
104
|
+
if (i + 30 < mints.length) await sleep(250);
|
|
105
|
+
}
|
|
106
|
+
return { data: best, failed: failed };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ---------- GeckoTerminal: fills two DexScreener gaps on Pump.fun ---------- */
|
|
110
|
+
// Both are optional extras: on any failure the market stays exactly as DexScreener gave it.
|
|
111
|
+
|
|
112
|
+
// DexScreener reports $0 liquidity for tokens still on Pump.fun's bonding curve. GeckoTerminal has the
|
|
113
|
+
// real figure for the same pool (its pool address is DexScreener's pairAddress). Mutates the pair.
|
|
114
|
+
export async function geckoLiquidity(client, pair) {
|
|
115
|
+
if (!pair || (pair.liquidity && pair.liquidity.usd) || !pair.pairAddress) return pair;
|
|
116
|
+
const j = await client.softJson(GECKO + "pools/" + pair.pairAddress, 6000);
|
|
117
|
+
const v = Number(j && j.data && j.data.attributes && j.data.attributes.reserve_in_usd) || 0;
|
|
118
|
+
if (v > 0) pair.liquidity = Object.assign({}, pair.liquidity, { usd: v });
|
|
119
|
+
return pair;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A token seconds old is not on DexScreener yet but is on GeckoTerminal: its most liquid pool, shaped
|
|
123
|
+
// like a DexScreener pair (what the engine reads: price, liquidity, volume, age, symbol), or null
|
|
124
|
+
export async function geckoPair(client, mint) {
|
|
125
|
+
const j = await client.softJson(GECKO + "tokens/" + mint + "/pools?page=1&include=base_token", 6000);
|
|
126
|
+
const p = j && j.data && j.data[0];
|
|
127
|
+
if (!p || !p.attributes) return null;
|
|
128
|
+
const a = p.attributes, rel = p.relationships || {}, t = ((j.included || [])[0] || {}).attributes || {};
|
|
129
|
+
return {
|
|
130
|
+
pairAddress: a.address, dexId: (rel.dex && rel.dex.data && rel.dex.data.id) || "",
|
|
131
|
+
baseToken: { address: mint, symbol: t.symbol, name: t.name },
|
|
132
|
+
priceUsd: String(a.base_token_price_usd || 0), liquidity: { usd: Number(a.reserve_in_usd) || 0 },
|
|
133
|
+
volume: { h24: Number(a.volume_usd && a.volume_usd.h24) || 0 }, pairCreatedAt: Date.parse(a.pool_created_at) || 0,
|
|
134
|
+
info: { imageUrl: t.image_url && !/missing/.test(t.image_url) ? t.image_url : null }, source: "geckoterminal"
|
|
135
|
+
};
|
|
136
|
+
}
|