solana-risk-engine 0.4.5 → 0.4.7
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/base.js +28 -28
- package/src/client.js +157 -157
- package/src/constants.js +4 -0
- package/src/holders.js +310 -274
- package/src/i18n/en.js +114 -114
- package/src/i18n/es.js +117 -117
- package/src/index.js +60 -60
- package/src/markets.js +141 -138
- package/src/tokens.js +123 -123
- package/src/wallet.js +94 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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/base.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { WALLET_BASE_URL } from "./constants.js";
|
|
2
|
-
|
|
3
|
-
// SolFlag's wallet base: what its collector learns from every launch it checks (exchanges, launch farms,
|
|
4
|
-
// distributors, serial creators), refreshed every 30 minutes. A known exchange is recognised at once,
|
|
5
|
-
// without asking the RPC how many transactions it makes
|
|
6
|
-
|
|
7
|
-
export const WALLET_TYPES = ["exchange", "granja", "repartidor", "creador en serie", "sniper"];
|
|
8
|
-
const BASE_TTL = 600000;
|
|
9
|
-
const caches = new WeakMap();
|
|
10
|
-
|
|
11
|
-
// The base, or null when it cannot be read (it is an extra: nothing depends on it). Each client keeps it
|
|
12
|
-
// 10 minutes per URL
|
|
13
|
-
export async function loadWalletBase(client, url) {
|
|
14
|
-
url = url || WALLET_BASE_URL;
|
|
15
|
-
if (!caches.has(client)) caches.set(client, {});
|
|
16
|
-
const cache = caches.get(client), c = cache[url];
|
|
17
|
-
if (c && Date.now() - c.at < BASE_TTL) return c.data;
|
|
18
|
-
const j = await client.softJson(url, 8000);
|
|
19
|
-
const data = j && j.wallets && typeof j.wallets === "object" ? j : null;
|
|
20
|
-
cache[url] = { at: Date.now(), data: data };
|
|
21
|
-
return data;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Type of a wallet in the base ("exchange", "granja"...) or null
|
|
25
|
-
export function knownType(base, addr) {
|
|
26
|
-
const w = base && base.wallets && base.wallets[addr];
|
|
27
|
-
return (w && WALLET_TYPES.indexOf(w.t) >= 0) ? w.t : null;
|
|
28
|
-
}
|
|
1
|
+
import { WALLET_BASE_URL } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
// SolFlag's wallet base: what its collector learns from every launch it checks (exchanges, launch farms,
|
|
4
|
+
// distributors, serial creators), refreshed every 30 minutes. A known exchange is recognised at once,
|
|
5
|
+
// without asking the RPC how many transactions it makes
|
|
6
|
+
|
|
7
|
+
export const WALLET_TYPES = ["exchange", "granja", "repartidor", "creador en serie", "sniper"];
|
|
8
|
+
const BASE_TTL = 600000;
|
|
9
|
+
const caches = new WeakMap();
|
|
10
|
+
|
|
11
|
+
// The base, or null when it cannot be read (it is an extra: nothing depends on it). Each client keeps it
|
|
12
|
+
// 10 minutes per URL
|
|
13
|
+
export async function loadWalletBase(client, url) {
|
|
14
|
+
url = url || WALLET_BASE_URL;
|
|
15
|
+
if (!caches.has(client)) caches.set(client, {});
|
|
16
|
+
const cache = caches.get(client), c = cache[url];
|
|
17
|
+
if (c && Date.now() - c.at < BASE_TTL) return c.data;
|
|
18
|
+
const j = await client.softJson(url, 8000);
|
|
19
|
+
const data = j && j.wallets && typeof j.wallets === "object" ? j : null;
|
|
20
|
+
cache[url] = { at: Date.now(), data: data };
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Type of a wallet in the base ("exchange", "granja"...) or null
|
|
25
|
+
export function knownType(base, addr) {
|
|
26
|
+
const w = base && base.wallets && base.wallets[addr];
|
|
27
|
+
return (w && WALLET_TYPES.indexOf(w.t) >= 0) ? w.t : null;
|
|
28
|
+
}
|
package/src/client.js
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
import { PUBLIC_RPC, VIBE_RPC } from "./constants.js";
|
|
2
|
-
import { ScanError, cancelled, isCancel } from "./errors.js";
|
|
3
|
-
import { sleep } from "./util.js";
|
|
4
|
-
|
|
5
|
-
// Methods PublicNode rejects with 403; if another one shows up it gets added at runtime
|
|
6
|
-
const PN_BLOCKED_DEFAULT = ["getTokenLargestAccounts", "getTokenAccountsByOwner", "getTokenSupply"];
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Creates the network client every engine function uses.
|
|
10
|
-
*
|
|
11
|
-
* @param {object} [opts]
|
|
12
|
-
* @param {string} [opts.rpcUrl] your own RPC URL (Helius, Triton...). Without it the free
|
|
13
|
-
* PublicNode + Solana Vibe Station combination is used.
|
|
14
|
-
* @param {string} [opts.heliusApiKey] Helius key for PnL. If omitted it is taken from rpcUrl when
|
|
15
|
-
* that URL is a Helius one (the key is never sent anywhere else).
|
|
16
|
-
* @param {AbortSignal} [opts.signal] aborts every pending request with a CANCELLED ScanError
|
|
17
|
-
* @param {Function} [opts.fetch] fetch implementation (defaults to globalThis.fetch)
|
|
18
|
-
* @param {number} [opts.retryDelayMs] first retry delay, doubled each time (default 1200)
|
|
19
|
-
* @param {number} [opts.vibeIntervalMs] spacing of the free Vibe Station queue (default 1100)
|
|
20
|
-
*/
|
|
21
|
-
export function createClient(opts) {
|
|
22
|
-
opts = opts || {};
|
|
23
|
-
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
24
|
-
if (!fetchImpl) throw new Error("no fetch available: pass opts.fetch or use Node 18+");
|
|
25
|
-
const rpcUrl = (opts.rpcUrl || "").trim() || PUBLIC_RPC;
|
|
26
|
-
const publicRpc = opts.publicRpc || PUBLIC_RPC;
|
|
27
|
-
const vibeRpc = opts.vibeRpc || VIBE_RPC;
|
|
28
|
-
const signal = opts.signal || null;
|
|
29
|
-
const retryDelay = opts.retryDelayMs === undefined ? 1200 : opts.retryDelayMs;
|
|
30
|
-
const vibeInterval = opts.vibeIntervalMs === undefined ? 1100 : opts.vibeIntervalMs;
|
|
31
|
-
const blocked = {};
|
|
32
|
-
PN_BLOCKED_DEFAULT.forEach(function (m) { blocked[m] = 1; });
|
|
33
|
-
let vibeNext = 0;
|
|
34
|
-
|
|
35
|
-
function checkAbort() {
|
|
36
|
-
if (signal && signal.aborted) throw cancelled();
|
|
37
|
-
}
|
|
38
|
-
function ownRpc() {
|
|
39
|
-
return rpcUrl !== publicRpc;
|
|
40
|
-
}
|
|
41
|
-
function heliusKey() {
|
|
42
|
-
if (opts.heliusApiKey) return opts.heliusApiKey;
|
|
43
|
-
// Only use the key if the configured RPC really is Helius: otherwise it is never sent to api.helius.xyz
|
|
44
|
-
try {
|
|
45
|
-
const u = new URL(rpcUrl);
|
|
46
|
-
if (!/(^|\.)helius-rpc\.com$|(^|\.)helius\.xyz$/i.test(u.hostname)) return null;
|
|
47
|
-
return u.searchParams.get("api-key") || null;
|
|
48
|
-
} catch (e) { return null; }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function rawFetch(url, init) {
|
|
52
|
-
checkAbort();
|
|
53
|
-
try {
|
|
54
|
-
return await fetchImpl(url, signal ? Object.assign({}, init, { signal: signal }) : init);
|
|
55
|
-
} catch (e) {
|
|
56
|
-
if (signal && signal.aborted) throw cancelled();
|
|
57
|
-
throw e;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// JSON fetch with retries on 429, 5xx and network errors (doubling the wait each time)
|
|
62
|
-
async function jfetch(url, init, tries) {
|
|
63
|
-
let wait = retryDelay, last = null;
|
|
64
|
-
tries = tries || 4;
|
|
65
|
-
for (let i = 0; i < tries; i++) {
|
|
66
|
-
checkAbort();
|
|
67
|
-
try {
|
|
68
|
-
const r = await rawFetch(url, init);
|
|
69
|
-
if (r.status === 429 || r.status >= 500) {
|
|
70
|
-
last = new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
|
|
71
|
-
await sleep(wait); wait *= 2; continue;
|
|
72
|
-
}
|
|
73
|
-
if (!r.ok) throw new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
|
|
74
|
-
return await r.json();
|
|
75
|
-
} catch (e) {
|
|
76
|
-
if (isCancel(e) || (e.code === "HTTP" && e.data.status >= 400 && e.data.status < 500)) throw e;
|
|
77
|
-
last = e; await sleep(wait); wait *= 2;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (last && last.code) throw last;
|
|
81
|
-
throw new ScanError("NETWORK", (last && last.message) || "no response", { cause: last && last.message });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Fetch that never throws (except on cancel): used for optional extras such as RugCheck,
|
|
85
|
-
// which must never block the main result if they fail, time out or are down
|
|
86
|
-
async function softJson(url, timeoutMs) {
|
|
87
|
-
checkAbort();
|
|
88
|
-
const ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
89
|
-
let onAbort = null;
|
|
90
|
-
if (ctrl && signal) { onAbort = function () { ctrl.abort(); }; signal.addEventListener("abort", onAbort); }
|
|
91
|
-
const to = ctrl ? setTimeout(function () { ctrl.abort(); }, timeoutMs || 8000) : null;
|
|
92
|
-
try {
|
|
93
|
-
const r = await fetchImpl(url, ctrl ? { signal: ctrl.signal } : undefined);
|
|
94
|
-
if (!r.ok) return null;
|
|
95
|
-
return await r.json();
|
|
96
|
-
} catch (e) {
|
|
97
|
-
return null;
|
|
98
|
-
} finally {
|
|
99
|
-
if (to) clearTimeout(to);
|
|
100
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
101
|
-
checkAbort();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function rpcAt(url, method, params, tries) {
|
|
106
|
-
const j = await jfetch(url, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: method, params: params })
|
|
110
|
-
}, tries);
|
|
111
|
-
if (j.error) throw new ScanError("RPC", j.error.message || "RPC error", { rpcCode: j.error.code });
|
|
112
|
-
return j.result;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function vibeSlot() {
|
|
116
|
-
const now = Date.now(), at = Math.max(now, vibeNext);
|
|
117
|
-
vibeNext = at + vibeInterval;
|
|
118
|
-
if (at > now) await sleep(at - now);
|
|
119
|
-
checkAbort();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function rpc(method, params) {
|
|
123
|
-
if (ownRpc()) return rpcAt(rpcUrl, method, params);
|
|
124
|
-
// PublicNode accepts at most 10 accounts per getMultipleAccounts: split and merge the result
|
|
125
|
-
if (method === "getMultipleAccounts" && params[0].length > 10) {
|
|
126
|
-
const parts = [];
|
|
127
|
-
for (let i = 0; i < params[0].length; i += 10) parts.push(rpc(method, [params[0].slice(i, i + 10)].concat(params.slice(1))));
|
|
128
|
-
const rs = await Promise.all(parts);
|
|
129
|
-
return { context: rs[0].context, value: [].concat.apply([], rs.map(function (r) { return r.value; })) };
|
|
130
|
-
}
|
|
131
|
-
if (!blocked[method]) {
|
|
132
|
-
try {
|
|
133
|
-
const res = await rpcAt(publicRpc, method, params);
|
|
134
|
-
// PublicNode only keeps a few days of history: a list that ends before filling the page, or a
|
|
135
|
-
// transaction it cannot find, may just be old. Vibe Station keeps it all, so it gets the last word
|
|
136
|
-
if (method === "getSignaturesForAddress" && Array.isArray(res) && res.length < ((params[1] && params[1].limit) || 1000)) {
|
|
137
|
-
await vibeSlot();
|
|
138
|
-
const full = await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
139
|
-
return Array.isArray(full) && full.length > res.length ? full : res;
|
|
140
|
-
}
|
|
141
|
-
if (method === "getTransaction" && res === null) {
|
|
142
|
-
await vibeSlot();
|
|
143
|
-
return await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
144
|
-
}
|
|
145
|
-
return res;
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
if (isCancel(e) || !(e.code === "HTTP" && (e.data.status === 401 || e.data.status === 403))) throw e;
|
|
149
|
-
blocked[method] = 1;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
await vibeSlot();
|
|
153
|
-
return rpcAt(vibeRpc, method, params, 6); // more retries: its limit sometimes trips even when the queue is respected
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return { rpc: rpc, jfetch: jfetch, softJson: softJson, ownRpc: ownRpc, heliusKey: heliusKey, checkAbort: checkAbort, rpcUrl: rpcUrl, retryDelayMs: opts.retryDelayMs };
|
|
157
|
-
}
|
|
1
|
+
import { PUBLIC_RPC, VIBE_RPC } from "./constants.js";
|
|
2
|
+
import { ScanError, cancelled, isCancel } from "./errors.js";
|
|
3
|
+
import { sleep } from "./util.js";
|
|
4
|
+
|
|
5
|
+
// Methods PublicNode rejects with 403; if another one shows up it gets added at runtime
|
|
6
|
+
const PN_BLOCKED_DEFAULT = ["getTokenLargestAccounts", "getTokenAccountsByOwner", "getTokenSupply"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates the network client every engine function uses.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @param {string} [opts.rpcUrl] your own RPC URL (Helius, Triton...). Without it the free
|
|
13
|
+
* PublicNode + Solana Vibe Station combination is used.
|
|
14
|
+
* @param {string} [opts.heliusApiKey] Helius key for PnL. If omitted it is taken from rpcUrl when
|
|
15
|
+
* that URL is a Helius one (the key is never sent anywhere else).
|
|
16
|
+
* @param {AbortSignal} [opts.signal] aborts every pending request with a CANCELLED ScanError
|
|
17
|
+
* @param {Function} [opts.fetch] fetch implementation (defaults to globalThis.fetch)
|
|
18
|
+
* @param {number} [opts.retryDelayMs] first retry delay, doubled each time (default 1200)
|
|
19
|
+
* @param {number} [opts.vibeIntervalMs] spacing of the free Vibe Station queue (default 1100)
|
|
20
|
+
*/
|
|
21
|
+
export function createClient(opts) {
|
|
22
|
+
opts = opts || {};
|
|
23
|
+
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
24
|
+
if (!fetchImpl) throw new Error("no fetch available: pass opts.fetch or use Node 18+");
|
|
25
|
+
const rpcUrl = (opts.rpcUrl || "").trim() || PUBLIC_RPC;
|
|
26
|
+
const publicRpc = opts.publicRpc || PUBLIC_RPC;
|
|
27
|
+
const vibeRpc = opts.vibeRpc || VIBE_RPC;
|
|
28
|
+
const signal = opts.signal || null;
|
|
29
|
+
const retryDelay = opts.retryDelayMs === undefined ? 1200 : opts.retryDelayMs;
|
|
30
|
+
const vibeInterval = opts.vibeIntervalMs === undefined ? 1100 : opts.vibeIntervalMs;
|
|
31
|
+
const blocked = {};
|
|
32
|
+
PN_BLOCKED_DEFAULT.forEach(function (m) { blocked[m] = 1; });
|
|
33
|
+
let vibeNext = 0;
|
|
34
|
+
|
|
35
|
+
function checkAbort() {
|
|
36
|
+
if (signal && signal.aborted) throw cancelled();
|
|
37
|
+
}
|
|
38
|
+
function ownRpc() {
|
|
39
|
+
return rpcUrl !== publicRpc;
|
|
40
|
+
}
|
|
41
|
+
function heliusKey() {
|
|
42
|
+
if (opts.heliusApiKey) return opts.heliusApiKey;
|
|
43
|
+
// Only use the key if the configured RPC really is Helius: otherwise it is never sent to api.helius.xyz
|
|
44
|
+
try {
|
|
45
|
+
const u = new URL(rpcUrl);
|
|
46
|
+
if (!/(^|\.)helius-rpc\.com$|(^|\.)helius\.xyz$/i.test(u.hostname)) return null;
|
|
47
|
+
return u.searchParams.get("api-key") || null;
|
|
48
|
+
} catch (e) { return null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function rawFetch(url, init) {
|
|
52
|
+
checkAbort();
|
|
53
|
+
try {
|
|
54
|
+
return await fetchImpl(url, signal ? Object.assign({}, init, { signal: signal }) : init);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (signal && signal.aborted) throw cancelled();
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// JSON fetch with retries on 429, 5xx and network errors (doubling the wait each time)
|
|
62
|
+
async function jfetch(url, init, tries) {
|
|
63
|
+
let wait = retryDelay, last = null;
|
|
64
|
+
tries = tries || 4;
|
|
65
|
+
for (let i = 0; i < tries; i++) {
|
|
66
|
+
checkAbort();
|
|
67
|
+
try {
|
|
68
|
+
const r = await rawFetch(url, init);
|
|
69
|
+
if (r.status === 429 || r.status >= 500) {
|
|
70
|
+
last = new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
|
|
71
|
+
await sleep(wait); wait *= 2; continue;
|
|
72
|
+
}
|
|
73
|
+
if (!r.ok) throw new ScanError("HTTP", "HTTP " + r.status, { status: r.status });
|
|
74
|
+
return await r.json();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (isCancel(e) || (e.code === "HTTP" && e.data.status >= 400 && e.data.status < 500)) throw e;
|
|
77
|
+
last = e; await sleep(wait); wait *= 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (last && last.code) throw last;
|
|
81
|
+
throw new ScanError("NETWORK", (last && last.message) || "no response", { cause: last && last.message });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fetch that never throws (except on cancel): used for optional extras such as RugCheck,
|
|
85
|
+
// which must never block the main result if they fail, time out or are down
|
|
86
|
+
async function softJson(url, timeoutMs) {
|
|
87
|
+
checkAbort();
|
|
88
|
+
const ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
89
|
+
let onAbort = null;
|
|
90
|
+
if (ctrl && signal) { onAbort = function () { ctrl.abort(); }; signal.addEventListener("abort", onAbort); }
|
|
91
|
+
const to = ctrl ? setTimeout(function () { ctrl.abort(); }, timeoutMs || 8000) : null;
|
|
92
|
+
try {
|
|
93
|
+
const r = await fetchImpl(url, ctrl ? { signal: ctrl.signal } : undefined);
|
|
94
|
+
if (!r.ok) return null;
|
|
95
|
+
return await r.json();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return null;
|
|
98
|
+
} finally {
|
|
99
|
+
if (to) clearTimeout(to);
|
|
100
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
101
|
+
checkAbort();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function rpcAt(url, method, params, tries) {
|
|
106
|
+
const j = await jfetch(url, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: method, params: params })
|
|
110
|
+
}, tries);
|
|
111
|
+
if (j.error) throw new ScanError("RPC", j.error.message || "RPC error", { rpcCode: j.error.code });
|
|
112
|
+
return j.result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function vibeSlot() {
|
|
116
|
+
const now = Date.now(), at = Math.max(now, vibeNext);
|
|
117
|
+
vibeNext = at + vibeInterval;
|
|
118
|
+
if (at > now) await sleep(at - now);
|
|
119
|
+
checkAbort();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function rpc(method, params) {
|
|
123
|
+
if (ownRpc()) return rpcAt(rpcUrl, method, params);
|
|
124
|
+
// PublicNode accepts at most 10 accounts per getMultipleAccounts: split and merge the result
|
|
125
|
+
if (method === "getMultipleAccounts" && params[0].length > 10) {
|
|
126
|
+
const parts = [];
|
|
127
|
+
for (let i = 0; i < params[0].length; i += 10) parts.push(rpc(method, [params[0].slice(i, i + 10)].concat(params.slice(1))));
|
|
128
|
+
const rs = await Promise.all(parts);
|
|
129
|
+
return { context: rs[0].context, value: [].concat.apply([], rs.map(function (r) { return r.value; })) };
|
|
130
|
+
}
|
|
131
|
+
if (!blocked[method]) {
|
|
132
|
+
try {
|
|
133
|
+
const res = await rpcAt(publicRpc, method, params);
|
|
134
|
+
// PublicNode only keeps a few days of history: a list that ends before filling the page, or a
|
|
135
|
+
// transaction it cannot find, may just be old. Vibe Station keeps it all, so it gets the last word
|
|
136
|
+
if (method === "getSignaturesForAddress" && Array.isArray(res) && res.length < ((params[1] && params[1].limit) || 1000)) {
|
|
137
|
+
await vibeSlot();
|
|
138
|
+
const full = await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
139
|
+
return Array.isArray(full) && full.length > res.length ? full : res;
|
|
140
|
+
}
|
|
141
|
+
if (method === "getTransaction" && res === null) {
|
|
142
|
+
await vibeSlot();
|
|
143
|
+
return await rpcAt(vibeRpc, method, params, 6).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
144
|
+
}
|
|
145
|
+
return res;
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
if (isCancel(e) || !(e.code === "HTTP" && (e.data.status === 401 || e.data.status === 403))) throw e;
|
|
149
|
+
blocked[method] = 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await vibeSlot();
|
|
153
|
+
return rpcAt(vibeRpc, method, params, 6); // more retries: its limit sometimes trips even when the queue is respected
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { rpc: rpc, jfetch: jfetch, softJson: softJson, ownRpc: ownRpc, heliusKey: heliusKey, checkAbort: checkAbort, rpcUrl: rpcUrl, retryDelayMs: opts.retryDelayMs };
|
|
157
|
+
}
|
package/src/constants.js
CHANGED
|
@@ -112,6 +112,10 @@ const BRAND_NAMES = [
|
|
|
112
112
|
// market at least this old AND this deep is not checked for brand names
|
|
113
113
|
export const BRAND_MATURE_DAYS = 30;
|
|
114
114
|
export const BRAND_MATURE_LIQ = 100000;
|
|
115
|
+
// COPYCAT: the token taken as the original must be a real market. Farms relaunch the same name and ticker
|
|
116
|
+
// many times a day, so the "oldest" is often another dead copy from an hour ago (registry 29/10: 209 of 320
|
|
117
|
+
// new tokens flagged). Below this liquidity the older token is not something worth copying
|
|
118
|
+
export const COPY_MIN_LIQ = 10000;
|
|
115
119
|
export const BRANDS = Object.fromEntries(BRAND_NAMES.map(function (n) { return [brandKey(n), n]; }));
|
|
116
120
|
|
|
117
121
|
// Official tokens put on Solana by exchanges and by Socios.com, by mint address (a copy always has another
|