@solard/cli 0.2.25 → 0.2.27
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": "@solard/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Command-line interface for the Solard SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@solana/web3.js": "^1.98.4",
|
|
22
|
-
"@solard/core": "0.2.
|
|
23
|
-
"@solard/sdk": "0.2.
|
|
22
|
+
"@solard/core": "0.2.27",
|
|
23
|
+
"@solard/sdk": "0.2.27",
|
|
24
24
|
"bs58": "^6.0.0",
|
|
25
25
|
"measure-fn": "5.2.0"
|
|
26
26
|
},
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { configure, createMeasure } from "measure-fn";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
subscribeLaunches,
|
|
5
|
-
subscribeTrades,
|
|
2
|
+
import slrd, {
|
|
6
3
|
type LaunchEvent,
|
|
7
4
|
type TradeEvent,
|
|
8
|
-
type
|
|
9
|
-
} from "@solard/
|
|
5
|
+
type TradeListener,
|
|
6
|
+
} from "@solard/sdk";
|
|
10
7
|
|
|
11
8
|
type Flags = Map<string, string>;
|
|
12
9
|
type Emit = (value: string) => void;
|
|
@@ -63,25 +60,15 @@ function venueList(flags: Flags): VenueId[] {
|
|
|
63
60
|
return out as VenueId[];
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
flag(flags, "rpc") ??
|
|
69
|
-
process.env.RPC_ENDPOINT?.trim() ??
|
|
70
|
-
process.env.SOLANA_RPC_URL?.trim() ??
|
|
71
|
-
process.env.HELIUS_RPC_URL?.trim();
|
|
72
|
-
if (!value)
|
|
63
|
+
function requireSingleEndpoint(flags: Flags): void {
|
|
64
|
+
if (flags.has("rpc") || flags.has("ws"))
|
|
73
65
|
throw new Error(
|
|
74
|
-
"
|
|
66
|
+
"Set only RPC_ENDPOINT in the environment. --rpc and --ws are intentionally unsupported.",
|
|
67
|
+
);
|
|
68
|
+
if (!process.env.RPC_ENDPOINT?.trim())
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Missing RPC_ENDPOINT. Set one Solana RPC URL in RPC_ENDPOINT; WebSocket access is derived from the same endpoint.",
|
|
75
71
|
);
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function wsUrl(flags: Flags): string | undefined {
|
|
80
|
-
return (
|
|
81
|
-
flag(flags, "ws") ??
|
|
82
|
-
process.env.SOLANA_WS_URL?.trim() ??
|
|
83
|
-
process.env.HELIUS_WS_URL?.trim()
|
|
84
|
-
);
|
|
85
72
|
}
|
|
86
73
|
|
|
87
74
|
export async function runLaunchWatchCommand(args: {
|
|
@@ -118,12 +105,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
118
105
|
const trackTtlMs = duration(flag(args.flags, "track-ttl"), 30 * 60_000);
|
|
119
106
|
const showNew = args.flags.has("show-new");
|
|
120
107
|
const showPrices = args.flags.has("trades") || args.flags.has("prices");
|
|
121
|
-
|
|
122
|
-
const ws = wsUrl(args.flags);
|
|
123
|
-
const connection = new Connection(
|
|
124
|
-
rpc,
|
|
125
|
-
ws ? { commitment: "confirmed", wsEndpoint: ws } : "confirmed",
|
|
126
|
-
);
|
|
108
|
+
requireSingleEndpoint(args.flags);
|
|
127
109
|
const states = new Map<string, WatchState>();
|
|
128
110
|
let launches = 0;
|
|
129
111
|
let prices = 0;
|
|
@@ -132,7 +114,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
132
114
|
let athNotifications = 0;
|
|
133
115
|
let ignoredMayhem = 0;
|
|
134
116
|
let stopped = false;
|
|
135
|
-
let tradeSubscription:
|
|
117
|
+
let tradeSubscription: TradeListener | null = null;
|
|
136
118
|
|
|
137
119
|
const onLaunch = (event: LaunchEvent) => {
|
|
138
120
|
if (!venues.includes(event.venue)) return;
|
|
@@ -150,7 +132,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
150
132
|
athMarketCapUsd: null,
|
|
151
133
|
lastNotifiedAthUsd: null,
|
|
152
134
|
});
|
|
153
|
-
void tradeSubscription?.
|
|
135
|
+
void tradeSubscription?.add(event.mint);
|
|
154
136
|
launches += 1;
|
|
155
137
|
if (showNew)
|
|
156
138
|
report("new launch", {
|
|
@@ -238,8 +220,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
238
220
|
};
|
|
239
221
|
|
|
240
222
|
const controller = new AbortController();
|
|
241
|
-
tradeSubscription = await
|
|
242
|
-
connection,
|
|
223
|
+
tradeSubscription = await slrd.listenTrades({
|
|
243
224
|
tokens: [],
|
|
244
225
|
venues: [
|
|
245
226
|
...(venues.includes("pump") ? (["pump", "pumpswap"] as const) : []),
|
|
@@ -248,13 +229,12 @@ export async function runLaunchWatchCommand(args: {
|
|
|
248
229
|
: []),
|
|
249
230
|
],
|
|
250
231
|
signal: controller.signal,
|
|
251
|
-
onTrade,
|
|
252
232
|
onStatus(event, data) {
|
|
253
233
|
report(`trades ${event}`, data ?? {});
|
|
254
234
|
},
|
|
255
235
|
});
|
|
256
|
-
|
|
257
|
-
|
|
236
|
+
tradeSubscription.onTrade(onTrade);
|
|
237
|
+
const launchSubscription = await slrd.subscribeLaunches({
|
|
258
238
|
venues,
|
|
259
239
|
signal: controller.signal,
|
|
260
240
|
onLaunch,
|
|
@@ -270,8 +250,8 @@ export async function runLaunchWatchCommand(args: {
|
|
|
270
250
|
process.once("SIGINT", stop);
|
|
271
251
|
process.once("SIGTERM", stop);
|
|
272
252
|
report("ready", {
|
|
273
|
-
rpc,
|
|
274
|
-
|
|
253
|
+
rpc: "RPC_ENDPOINT",
|
|
254
|
+
websocket: "derived-from-RPC_ENDPOINT",
|
|
275
255
|
venues,
|
|
276
256
|
minMarketCapUsd,
|
|
277
257
|
athStepPct,
|
|
@@ -285,7 +265,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
285
265
|
for (const [mint, state] of states) {
|
|
286
266
|
if (!state.qualified && now - state.lastSeenAtMs > trackTtlMs) {
|
|
287
267
|
states.delete(mint);
|
|
288
|
-
void tradeSubscription?.
|
|
268
|
+
void tradeSubscription?.remove(mint);
|
|
289
269
|
}
|
|
290
270
|
}
|
|
291
271
|
report("heartbeat", {
|
|
@@ -296,7 +276,7 @@ export async function runLaunchWatchCommand(args: {
|
|
|
296
276
|
prices,
|
|
297
277
|
thresholdNotifications,
|
|
298
278
|
athNotifications,
|
|
299
|
-
watchedTokens: tradeSubscription?.
|
|
279
|
+
watchedTokens: tradeSubscription?.list().length ?? 0,
|
|
300
280
|
});
|
|
301
281
|
},
|
|
302
282
|
Math.max(1_000, Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000))),
|
package/src/price-feed-client.ts
CHANGED
|
@@ -14,18 +14,22 @@ function text(value: unknown): string {
|
|
|
14
14
|
return String(value ?? "");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function errorText(error: unknown): string {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export type PriceFeedClient = {
|
|
18
22
|
readonly connected: boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
subscribeMints(mints: string | readonly string[]): void;
|
|
24
|
+
unsubscribeMints(mints: string | readonly string[]): void;
|
|
25
|
+
listMints(): string[];
|
|
22
26
|
close(): void;
|
|
23
27
|
closed: Promise<void>;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export async function connectPriceFeed(args: {
|
|
27
31
|
url: string;
|
|
28
|
-
|
|
32
|
+
mints?: string | readonly string[];
|
|
29
33
|
onMessage: (message: PriceFeedMessage) => void | Promise<void>;
|
|
30
34
|
onStatus?: (event: string, data?: Record<string, unknown>) => void;
|
|
31
35
|
signal?: AbortSignal;
|
|
@@ -33,18 +37,26 @@ export async function connectPriceFeed(args: {
|
|
|
33
37
|
let stopped = false;
|
|
34
38
|
let connected = false;
|
|
35
39
|
let ws: WebSocket | null = null;
|
|
36
|
-
const desiredMints = new Set(
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const desiredMints = new Set(
|
|
41
|
+
(typeof args.mints === "string" ? [args.mints] : [...(args.mints ?? [])])
|
|
42
|
+
.map((mint) => mint.trim())
|
|
43
|
+
.filter(Boolean),
|
|
44
|
+
);
|
|
39
45
|
let reconnectDelay = 250;
|
|
40
46
|
let resolveClosed!: () => void;
|
|
41
47
|
const closed = new Promise<void>((resolve) => {
|
|
42
48
|
resolveClosed = resolve;
|
|
43
49
|
});
|
|
44
50
|
|
|
45
|
-
const
|
|
51
|
+
const send = (command: PriceFeedCommand): void => {
|
|
52
|
+
if (!connected || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
53
|
+
ws.send(JSON.stringify(command));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const stop = (): void => {
|
|
46
57
|
if (stopped) return;
|
|
47
58
|
stopped = true;
|
|
59
|
+
connected = false;
|
|
48
60
|
try {
|
|
49
61
|
ws?.close();
|
|
50
62
|
} catch {}
|
|
@@ -60,13 +72,12 @@ export async function connectPriceFeed(args: {
|
|
|
60
72
|
await new Promise<void>((resolve, reject) => {
|
|
61
73
|
let settled = false;
|
|
62
74
|
const timeout = setTimeout(() => {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
try {
|
|
78
|
+
socket.close();
|
|
79
|
+
} catch {}
|
|
80
|
+
reject(new Error("price feed websocket handshake timed out"));
|
|
70
81
|
}, 5_000);
|
|
71
82
|
socket.addEventListener("open", () => {
|
|
72
83
|
if (settled) return;
|
|
@@ -74,22 +85,17 @@ export async function connectPriceFeed(args: {
|
|
|
74
85
|
clearTimeout(timeout);
|
|
75
86
|
connected = true;
|
|
76
87
|
reconnectDelay = 250;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
launches: desiredLaunches,
|
|
82
|
-
allPrices: desiredAllPrices,
|
|
83
|
-
} satisfies PriceFeedCommand),
|
|
84
|
-
);
|
|
85
|
-
args.onStatus?.("connected", { url: args.url });
|
|
88
|
+
const mints = [...desiredMints];
|
|
89
|
+
if (mints.length)
|
|
90
|
+
socket.send(JSON.stringify({ op: "subscribe", mints }));
|
|
91
|
+
args.onStatus?.("connected", { url: args.url, mints });
|
|
86
92
|
resolve();
|
|
87
93
|
});
|
|
88
94
|
socket.addEventListener("error", () => {
|
|
89
95
|
if (settled) return;
|
|
90
96
|
settled = true;
|
|
91
97
|
clearTimeout(timeout);
|
|
92
|
-
reject(new Error("feed websocket error"));
|
|
98
|
+
reject(new Error("price feed websocket error"));
|
|
93
99
|
});
|
|
94
100
|
});
|
|
95
101
|
|
|
@@ -102,13 +108,12 @@ export async function connectPriceFeed(args: {
|
|
|
102
108
|
return;
|
|
103
109
|
}
|
|
104
110
|
void Promise.resolve(args.onMessage(message)).catch((error) =>
|
|
105
|
-
args.onStatus?.("callback-error", {
|
|
106
|
-
error: error instanceof Error ? error.message : String(error),
|
|
107
|
-
}),
|
|
111
|
+
args.onStatus?.("callback-error", { error: errorText(error) }),
|
|
108
112
|
);
|
|
109
113
|
});
|
|
110
114
|
socket.addEventListener("close", () => {
|
|
111
115
|
connected = false;
|
|
116
|
+
args.onStatus?.("disconnected", { url: args.url });
|
|
112
117
|
resolve();
|
|
113
118
|
});
|
|
114
119
|
socket.addEventListener("error", () => {
|
|
@@ -118,7 +123,7 @@ export async function connectPriceFeed(args: {
|
|
|
118
123
|
} catch (error) {
|
|
119
124
|
connected = false;
|
|
120
125
|
args.onStatus?.("connect-error", {
|
|
121
|
-
error:
|
|
126
|
+
error: errorText(error),
|
|
122
127
|
retryInMs: reconnectDelay,
|
|
123
128
|
});
|
|
124
129
|
}
|
|
@@ -136,32 +141,33 @@ export async function connectPriceFeed(args: {
|
|
|
136
141
|
if (!connected && !stopped) {
|
|
137
142
|
stop();
|
|
138
143
|
throw new Error(
|
|
139
|
-
`Could not connect to Solard price feed at ${args.url}. Start
|
|
144
|
+
`Could not connect to Solard price feed at ${args.url}. Start the shared price feed before starting traders.`,
|
|
140
145
|
);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
const send = (command: PriceFeedCommand) => {
|
|
144
|
-
if (command.op === "subscribe") {
|
|
145
|
-
for (const mint of command.mints ?? []) desiredMints.add(mint);
|
|
146
|
-
if (command.launches === true) desiredLaunches = true;
|
|
147
|
-
if (command.allPrices === true) desiredAllPrices = true;
|
|
148
|
-
} else if (command.op === "unsubscribe") {
|
|
149
|
-
for (const mint of command.mints ?? []) desiredMints.delete(mint);
|
|
150
|
-
}
|
|
151
|
-
if (!connected || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
152
|
-
ws.send(JSON.stringify(command));
|
|
153
|
-
};
|
|
154
148
|
return {
|
|
155
149
|
get connected() {
|
|
156
150
|
return connected;
|
|
157
151
|
},
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
subscribeMints(value) {
|
|
153
|
+
const mints = (typeof value === "string" ? [value] : [...value])
|
|
154
|
+
.map((mint) => mint.trim())
|
|
155
|
+
.filter((mint) => mint && !desiredMints.has(mint));
|
|
156
|
+
if (!mints.length) return;
|
|
157
|
+
for (const mint of mints) desiredMints.add(mint);
|
|
160
158
|
send({ op: "subscribe", mints });
|
|
161
159
|
},
|
|
162
|
-
unsubscribeMints(
|
|
160
|
+
unsubscribeMints(value) {
|
|
161
|
+
const mints = (typeof value === "string" ? [value] : [...value])
|
|
162
|
+
.map((mint) => mint.trim())
|
|
163
|
+
.filter((mint) => mint && desiredMints.has(mint));
|
|
164
|
+
if (!mints.length) return;
|
|
165
|
+
for (const mint of mints) desiredMints.delete(mint);
|
|
163
166
|
send({ op: "unsubscribe", mints });
|
|
164
167
|
},
|
|
168
|
+
listMints() {
|
|
169
|
+
return [...desiredMints];
|
|
170
|
+
},
|
|
165
171
|
close: stop,
|
|
166
172
|
closed,
|
|
167
173
|
};
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
"pump" | "pumpswap" | "raydium-launchlab" | "rpc-fallback";
|
|
1
|
+
import type { SolUsdSource, TradeSide, TradeVenue } from "@solard/sdk";
|
|
3
2
|
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
export type PriceFeedMarket = {
|
|
4
|
+
quoteMint: string;
|
|
5
|
+
baseDecimals: number;
|
|
6
|
+
quoteDecimals: number;
|
|
7
|
+
supply: number;
|
|
8
|
+
baseReserve: number;
|
|
9
|
+
quoteReserve: number;
|
|
10
|
+
priceQuotePerToken: number;
|
|
11
|
+
marketCapQuote: number;
|
|
12
|
+
priceSol: number | null;
|
|
13
|
+
marketCapSol: number | null;
|
|
14
|
+
solUsd: number | null;
|
|
15
|
+
solUsdSource: SolUsdSource | null;
|
|
16
|
+
solUsdAtMs: number | null;
|
|
17
|
+
priceUsd: number | null;
|
|
18
|
+
marketCapUsd: number | null;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type PriceFeedPrice = {
|
|
21
22
|
type: "price";
|
|
22
23
|
atMs: number;
|
|
23
|
-
signature: string
|
|
24
|
-
slot: number
|
|
24
|
+
signature: string;
|
|
25
|
+
slot: number;
|
|
25
26
|
mint: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
source: string;
|
|
27
|
+
pool: string | null;
|
|
28
|
+
venue: TradeVenue;
|
|
29
|
+
side: TradeSide | null;
|
|
30
|
+
market: PriceFeedMarket;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type PriceFeedStatus = {
|
|
@@ -37,15 +37,9 @@ export type PriceFeedStatus = {
|
|
|
37
37
|
data?: Record<string, unknown>;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export type PriceFeedMessage =
|
|
41
|
-
PriceFeedLaunch | PriceFeedPrice | PriceFeedStatus;
|
|
40
|
+
export type PriceFeedMessage = PriceFeedPrice | PriceFeedStatus;
|
|
42
41
|
|
|
43
42
|
export type PriceFeedCommand =
|
|
44
|
-
| {
|
|
45
|
-
|
|
46
|
-
mints?: string[];
|
|
47
|
-
launches?: boolean;
|
|
48
|
-
allPrices?: boolean;
|
|
49
|
-
}
|
|
50
|
-
| { op: "unsubscribe"; mints?: string[] }
|
|
43
|
+
| { op: "subscribe"; mints: string[] }
|
|
44
|
+
| { op: "unsubscribe"; mints: string[] }
|
|
51
45
|
| { op: "ping" };
|
|
@@ -8,14 +8,13 @@ const source = readFileSync(
|
|
|
8
8
|
);
|
|
9
9
|
|
|
10
10
|
describe("price feed application boundary", () => {
|
|
11
|
-
test("
|
|
12
|
-
expect(source).toContain("
|
|
13
|
-
expect(source).toContain("
|
|
14
|
-
expect(source).toContain(
|
|
11
|
+
test("owns one sdk trade subscription and changes its token set dynamically", () => {
|
|
12
|
+
expect(source).toContain("slrd.listenTrades");
|
|
13
|
+
expect(source).toContain("tradeSubscription.add");
|
|
14
|
+
expect(source).toContain("tradeSubscription?.remove");
|
|
15
|
+
expect(source).toContain("const refs = new Map<string, number>()");
|
|
16
|
+
expect(source).not.toContain("subscribeLaunches");
|
|
15
17
|
expect(source).not.toContain('from "@solard/core"');
|
|
16
|
-
expect(source).not.toMatch(
|
|
17
|
-
/PUMP_(?:CREATE|TRADE)_EVENT|PUMPSWAP_(?:BUY|SELL|CREATE)_EVENT|LAUNCHLAB_(?:CREATE|TRADE)_EVENT/,
|
|
18
|
-
);
|
|
19
18
|
expect(source).not.toMatch(/logsSubscribe|onLogs\(/);
|
|
20
19
|
});
|
|
21
20
|
});
|
package/src/price-feed-server.ts
CHANGED
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
import { configure, createMeasure } from "measure-fn";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
getSolUsdPrice,
|
|
5
|
-
subscribeLaunches,
|
|
6
|
-
subscribeTrades,
|
|
7
|
-
type LaunchEvent,
|
|
8
|
-
type TradeEvent,
|
|
9
|
-
type TradeSubscription,
|
|
10
|
-
} from "@solard/sdk";
|
|
2
|
+
import slrd, { type TradeEvent, type TradeListener } from "@solard/sdk";
|
|
11
3
|
import type {
|
|
12
4
|
PriceFeedCommand,
|
|
13
|
-
PriceFeedLaunch,
|
|
14
5
|
PriceFeedMessage,
|
|
15
6
|
PriceFeedPrice,
|
|
16
7
|
} from "./price-feed-protocol.ts";
|
|
17
8
|
|
|
18
9
|
type Flags = Map<string, string>;
|
|
19
10
|
type Emit = (value: string) => void;
|
|
20
|
-
type Client = {
|
|
21
|
-
mints: Set<string>;
|
|
22
|
-
launches: boolean;
|
|
23
|
-
allPrices: boolean;
|
|
24
|
-
};
|
|
25
|
-
type MintState = {
|
|
26
|
-
supplyUi: number | null;
|
|
27
|
-
isMayhemMode: boolean | null;
|
|
28
|
-
};
|
|
11
|
+
type Client = { mints: Set<string> };
|
|
29
12
|
|
|
30
13
|
function flag(flags: Flags, key: string): string | undefined {
|
|
31
14
|
const value = flags.get(key);
|
|
@@ -40,56 +23,60 @@ function numberFlag(flags: Flags, key: string, fallback: number): number {
|
|
|
40
23
|
return value;
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
flag(flags, "rpc") ??
|
|
46
|
-
process.env.RPC_ENDPOINT?.trim() ??
|
|
47
|
-
process.env.SOLANA_RPC_URL?.trim() ??
|
|
48
|
-
process.env.HELIUS_RPC_URL?.trim();
|
|
49
|
-
if (!value) {
|
|
26
|
+
function feedAddress(flags: Flags): { host: string; port: number } {
|
|
27
|
+
if (flags.has("rpc") || flags.has("ws")) {
|
|
50
28
|
throw new Error(
|
|
51
|
-
"
|
|
29
|
+
"Set only RPC_ENDPOINT in the environment. --rpc and --ws are intentionally unsupported.",
|
|
52
30
|
);
|
|
53
31
|
}
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function websocketUrl(flags: Flags): string | undefined {
|
|
58
|
-
return (
|
|
59
|
-
flag(flags, "ws") ??
|
|
60
|
-
process.env.SOLANA_WS_URL?.trim() ??
|
|
61
|
-
process.env.HELIUS_WS_URL?.trim()
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function feedAddress(flags: Flags): { host: string; port: number } {
|
|
66
32
|
const host = flag(flags, "host") ?? "127.0.0.1";
|
|
33
|
+
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
|
|
34
|
+
throw new Error("The trader price feed is loopback-only");
|
|
35
|
+
}
|
|
67
36
|
const port = Math.trunc(numberFlag(flags, "port", 8788));
|
|
68
37
|
if (!(port > 0 && port <= 65_535)) throw new Error("--port must be 1..65535");
|
|
69
38
|
return { host, port };
|
|
70
39
|
}
|
|
71
40
|
|
|
72
|
-
function publicEndpoint(value: string): string {
|
|
73
|
-
try {
|
|
74
|
-
const url = new URL(value);
|
|
75
|
-
for (const key of [...url.searchParams.keys()]) {
|
|
76
|
-
if (/key|token|secret|auth/i.test(key)) {
|
|
77
|
-
url.searchParams.set(key, "<redacted>");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return url.toString();
|
|
81
|
-
} catch {
|
|
82
|
-
return value.replace(
|
|
83
|
-
/([?&](?:api-?key|token|secret|auth)=)[^&]+/gi,
|
|
84
|
-
"$1<redacted>",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
41
|
function errorText(error: unknown): string {
|
|
90
42
|
return error instanceof Error ? error.message : String(error);
|
|
91
43
|
}
|
|
92
44
|
|
|
45
|
+
function uniqueMints(value: unknown): string[] {
|
|
46
|
+
if (!Array.isArray(value)) throw new Error("mints must be an array");
|
|
47
|
+
return [...new Set(value.map((mint) => String(mint).trim()).filter(Boolean))];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function priceMessage(event: TradeEvent): PriceFeedPrice {
|
|
51
|
+
return {
|
|
52
|
+
type: "price",
|
|
53
|
+
atMs: event.atMs,
|
|
54
|
+
signature: event.signature,
|
|
55
|
+
slot: event.slot,
|
|
56
|
+
mint: event.mint,
|
|
57
|
+
pool: event.pool,
|
|
58
|
+
venue: event.venue,
|
|
59
|
+
side: event.side,
|
|
60
|
+
market: {
|
|
61
|
+
quoteMint: event.market.quoteMint,
|
|
62
|
+
baseDecimals: event.market.baseDecimals,
|
|
63
|
+
quoteDecimals: event.market.quoteDecimals,
|
|
64
|
+
supply: event.market.supply,
|
|
65
|
+
baseReserve: event.market.baseReserve,
|
|
66
|
+
quoteReserve: event.market.quoteReserve,
|
|
67
|
+
priceQuotePerToken: event.market.priceQuotePerToken,
|
|
68
|
+
marketCapQuote: event.market.marketCapQuote,
|
|
69
|
+
priceSol: event.market.priceSol,
|
|
70
|
+
marketCapSol: event.market.marketCapSol,
|
|
71
|
+
solUsd: event.market.solUsd,
|
|
72
|
+
solUsdSource: event.market.solUsdSource,
|
|
73
|
+
solUsdAtMs: event.market.solUsdAtMs,
|
|
74
|
+
priceUsd: event.market.priceUsd,
|
|
75
|
+
marketCapUsd: event.market.marketCapUsd,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
93
80
|
export async function runPriceFeedServerCommand(args: {
|
|
94
81
|
flags: Flags;
|
|
95
82
|
emit: Emit;
|
|
@@ -107,34 +94,14 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
107
94
|
): T =>
|
|
108
95
|
m.sync({ start: () => label, end: (result: T) => result }, () => value);
|
|
109
96
|
const { host, port } = feedAddress(args.flags);
|
|
110
|
-
const rpc = rpcUrl(args.flags);
|
|
111
|
-
const ws = websocketUrl(args.flags);
|
|
112
|
-
const includeMayhem = args.flags.has("include-mayhem");
|
|
113
|
-
const connection = new Connection(
|
|
114
|
-
rpc,
|
|
115
|
-
ws ? { commitment: "confirmed", wsEndpoint: ws } : "confirmed",
|
|
116
|
-
);
|
|
117
97
|
const controller = new AbortController();
|
|
118
98
|
const clients = new Map<unknown, Client>();
|
|
119
|
-
const
|
|
99
|
+
const refs = new Map<string, number>();
|
|
120
100
|
const latest = new Map<string, PriceFeedPrice>();
|
|
121
|
-
|
|
122
|
-
const discoveredMints = new Set<string>();
|
|
123
|
-
const explicitMintRefs = new Map<string, number>();
|
|
124
|
-
let tradeSubscription: TradeSubscription | null = null;
|
|
125
|
-
let allPriceClients = 0;
|
|
126
|
-
let solUsd: number | null = null;
|
|
127
|
-
try {
|
|
128
|
-
const quote = await getSolUsdPrice();
|
|
129
|
-
solUsd = quote.price;
|
|
130
|
-
report("sol-usd", quote);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
report("sol-usd-unavailable", { error: errorText(error) });
|
|
133
|
-
}
|
|
134
|
-
let launchCount = 0;
|
|
101
|
+
let tradeSubscription: TradeListener | null = null;
|
|
135
102
|
let tradeCount = 0;
|
|
136
|
-
let priceCount = 0;
|
|
137
103
|
let stopped = false;
|
|
104
|
+
let commandQueue = Promise.resolve();
|
|
138
105
|
|
|
139
106
|
const send = (
|
|
140
107
|
socket: { send(value: string): unknown },
|
|
@@ -145,112 +112,40 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
145
112
|
} catch {}
|
|
146
113
|
};
|
|
147
114
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (value.type === "price") {
|
|
156
|
-
if (client.allPrices || client.mints.has(value.mint))
|
|
157
|
-
send(target, value);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
send(target, value);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const shouldWatchMint = (mint: string): boolean =>
|
|
165
|
-
(explicitMintRefs.get(mint) ?? 0) > 0 ||
|
|
166
|
-
(allPriceClients > 0 && discoveredMints.has(mint));
|
|
167
|
-
|
|
168
|
-
const syncMintSubscription = async (mint: string): Promise<void> => {
|
|
169
|
-
if (!tradeSubscription) return;
|
|
170
|
-
const watched = tradeSubscription.hasToken(mint);
|
|
171
|
-
const desired = shouldWatchMint(mint);
|
|
172
|
-
if (desired && !watched) await tradeSubscription.addTokens(mint);
|
|
173
|
-
if (!desired && watched) await tradeSubscription.removeTokens(mint);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const syncDiscoveredSubscriptions = async (): Promise<void> => {
|
|
177
|
-
await Promise.allSettled(
|
|
178
|
-
[...discoveredMints].map((mint) => syncMintSubscription(mint)),
|
|
179
|
-
);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const onLaunch = async (event: LaunchEvent): Promise<void> => {
|
|
183
|
-
if (event.isMayhemMode === true && !includeMayhem) return;
|
|
184
|
-
const value: PriceFeedLaunch = {
|
|
185
|
-
type: "launch",
|
|
186
|
-
atMs: event.atMs,
|
|
187
|
-
signature: event.signature,
|
|
188
|
-
slot: event.slot,
|
|
189
|
-
mint: event.mint,
|
|
190
|
-
venue: event.venue,
|
|
191
|
-
decimals: event.decimals,
|
|
192
|
-
supplyUi: event.supplyUi,
|
|
193
|
-
quoteMint: event.quoteMint,
|
|
194
|
-
pool: event.pool,
|
|
195
|
-
name: event.name,
|
|
196
|
-
symbol: event.symbol,
|
|
197
|
-
isMayhemMode: event.isMayhemMode,
|
|
198
|
-
};
|
|
199
|
-
mintState.set(event.mint, {
|
|
200
|
-
supplyUi: event.supplyUi,
|
|
201
|
-
isMayhemMode: event.isMayhemMode,
|
|
202
|
-
});
|
|
203
|
-
launches.set(event.mint, value);
|
|
204
|
-
discoveredMints.add(event.mint);
|
|
205
|
-
launchCount += 1;
|
|
206
|
-
broadcast(value);
|
|
207
|
-
await syncMintSubscription(event.mint);
|
|
208
|
-
};
|
|
115
|
+
const status = (
|
|
116
|
+
socket: { send(value: string): unknown },
|
|
117
|
+
event: string,
|
|
118
|
+
data?: Record<string, unknown>,
|
|
119
|
+
): void => send(socket, { type: "status", atMs: Date.now(), event, data });
|
|
209
120
|
|
|
210
121
|
const onTrade = (event: TradeEvent): void => {
|
|
211
122
|
tradeCount += 1;
|
|
212
|
-
const
|
|
213
|
-
if (state?.isMayhemMode === true && !includeMayhem) return;
|
|
214
|
-
solUsd = event.market.solUsd ?? solUsd;
|
|
215
|
-
const value: PriceFeedPrice = {
|
|
216
|
-
type: "price",
|
|
217
|
-
atMs: event.atMs,
|
|
218
|
-
signature: event.signature,
|
|
219
|
-
slot: event.slot,
|
|
220
|
-
mint: event.mint,
|
|
221
|
-
venue: event.venue,
|
|
222
|
-
priceSol: event.market.priceSol,
|
|
223
|
-
priceUsd: event.market.priceUsd,
|
|
224
|
-
marketCapUsd: event.market.marketCapUsd,
|
|
225
|
-
source: `${event.venue}-trade`,
|
|
226
|
-
};
|
|
123
|
+
const value = priceMessage(event);
|
|
227
124
|
latest.set(event.mint, value);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const updateExplicitMintRef = async (
|
|
233
|
-
mint: string,
|
|
234
|
-
delta: 1 | -1,
|
|
235
|
-
): Promise<void> => {
|
|
236
|
-
const next = Math.max(0, (explicitMintRefs.get(mint) ?? 0) + delta);
|
|
237
|
-
if (next === 0) explicitMintRefs.delete(mint);
|
|
238
|
-
else explicitMintRefs.set(mint, next);
|
|
239
|
-
await syncMintSubscription(mint);
|
|
125
|
+
for (const [socket, client] of clients) {
|
|
126
|
+
if (!client.mints.has(event.mint)) continue;
|
|
127
|
+
send(socket as { send(value: string): unknown }, value);
|
|
128
|
+
}
|
|
240
129
|
};
|
|
241
130
|
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
131
|
+
const addRef = async (mint: string): Promise<void> => {
|
|
132
|
+
const current = refs.get(mint) ?? 0;
|
|
133
|
+
if (current === 0) {
|
|
134
|
+
if (!tradeSubscription) throw new Error("Price feed is not ready");
|
|
135
|
+
await tradeSubscription.add(mint);
|
|
136
|
+
}
|
|
137
|
+
refs.set(mint, current + 1);
|
|
247
138
|
};
|
|
248
139
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
140
|
+
const removeRef = async (mint: string): Promise<void> => {
|
|
141
|
+
const current = refs.get(mint) ?? 0;
|
|
142
|
+
if (current <= 0) return;
|
|
143
|
+
if (current === 1) {
|
|
144
|
+
refs.delete(mint);
|
|
145
|
+
await tradeSubscription?.remove(mint);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
refs.set(mint, current - 1);
|
|
254
149
|
};
|
|
255
150
|
|
|
256
151
|
const applyCommand = async (
|
|
@@ -261,65 +156,56 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
261
156
|
if (!client) return;
|
|
262
157
|
const target = socket as { send(value: string): unknown };
|
|
263
158
|
if (command.op === "ping") {
|
|
264
|
-
|
|
159
|
+
status(target, "pong", {
|
|
160
|
+
watchedMints: client.mints.size,
|
|
161
|
+
upstreamMints: refs.size,
|
|
162
|
+
});
|
|
265
163
|
return;
|
|
266
164
|
}
|
|
267
|
-
const mints
|
|
165
|
+
const mints = uniqueMints(command.mints);
|
|
268
166
|
if (command.op === "subscribe") {
|
|
269
|
-
if (command.launches === true && !client.launches) {
|
|
270
|
-
client.launches = true;
|
|
271
|
-
for (const value of launches.values()) send(target, value);
|
|
272
|
-
}
|
|
273
|
-
if (command.allPrices === true && !client.allPrices) {
|
|
274
|
-
await enableAllPrices(client);
|
|
275
|
-
for (const value of latest.values()) send(target, value);
|
|
276
|
-
}
|
|
277
167
|
for (const mint of mints) {
|
|
278
168
|
if (!client.mints.has(mint)) {
|
|
169
|
+
await addRef(mint);
|
|
279
170
|
client.mints.add(mint);
|
|
280
|
-
await updateExplicitMintRef(mint, 1);
|
|
281
171
|
}
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
172
|
+
const cached = latest.get(mint);
|
|
173
|
+
if (cached) send(target, cached);
|
|
284
174
|
}
|
|
175
|
+
status(target, "subscribed", {
|
|
176
|
+
mints,
|
|
177
|
+
watchedMints: client.mints.size,
|
|
178
|
+
upstreamMints: refs.size,
|
|
179
|
+
});
|
|
285
180
|
return;
|
|
286
181
|
}
|
|
287
182
|
for (const mint of mints) {
|
|
288
183
|
if (!client.mints.delete(mint)) continue;
|
|
289
|
-
await
|
|
184
|
+
await removeRef(mint);
|
|
290
185
|
}
|
|
186
|
+
status(target, "unsubscribed", {
|
|
187
|
+
mints,
|
|
188
|
+
watchedMints: client.mints.size,
|
|
189
|
+
upstreamMints: refs.size,
|
|
190
|
+
});
|
|
291
191
|
};
|
|
292
192
|
|
|
293
193
|
const releaseClient = async (socket: unknown): Promise<void> => {
|
|
294
194
|
const client = clients.get(socket);
|
|
295
195
|
clients.delete(socket);
|
|
296
196
|
if (!client) return;
|
|
297
|
-
|
|
298
|
-
await Promise.allSettled(
|
|
299
|
-
[...client.mints].map((mint) => updateExplicitMintRef(mint, -1)),
|
|
300
|
-
);
|
|
197
|
+
for (const mint of client.mints) await removeRef(mint);
|
|
301
198
|
};
|
|
302
199
|
|
|
303
|
-
tradeSubscription = await
|
|
304
|
-
connection,
|
|
200
|
+
tradeSubscription = await slrd.listenTrades({
|
|
305
201
|
tokens: [],
|
|
306
202
|
commitment: "confirmed",
|
|
307
203
|
signal: controller.signal,
|
|
308
|
-
onTrade,
|
|
309
204
|
onStatus(event, data) {
|
|
310
205
|
report(`trades ${event}`, data ?? {});
|
|
311
206
|
},
|
|
312
207
|
});
|
|
313
|
-
|
|
314
|
-
connection,
|
|
315
|
-
commitment: "confirmed",
|
|
316
|
-
metadata: false,
|
|
317
|
-
signal: controller.signal,
|
|
318
|
-
onLaunch,
|
|
319
|
-
onStatus(event, data) {
|
|
320
|
-
report(`launches ${event}`, data ?? {});
|
|
321
|
-
},
|
|
322
|
-
});
|
|
208
|
+
tradeSubscription.onTrade(onTrade);
|
|
323
209
|
|
|
324
210
|
const server = Bun.serve({
|
|
325
211
|
hostname: host,
|
|
@@ -335,54 +221,52 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
335
221
|
if (url.pathname === "/health") {
|
|
336
222
|
return Response.json({
|
|
337
223
|
ok: true,
|
|
224
|
+
service: "solard-price-feed",
|
|
338
225
|
clients: clients.size,
|
|
339
|
-
|
|
226
|
+
upstreamMints: refs.size,
|
|
227
|
+
watchedTokens: tradeSubscription?.list().length ?? 0,
|
|
228
|
+
cachedPrices: latest.size,
|
|
340
229
|
tradeCount,
|
|
341
|
-
|
|
342
|
-
cachedLaunches: launches.size,
|
|
343
|
-
latestPrices: latest.size,
|
|
344
|
-
discoveredMints: discoveredMints.size,
|
|
345
|
-
explicitMints: explicitMintRefs.size,
|
|
346
|
-
allPriceClients,
|
|
347
|
-
watchedTokens: tradeSubscription?.listTokens().length ?? 0,
|
|
348
|
-
solUsd,
|
|
230
|
+
subscriptions: Object.fromEntries(refs),
|
|
349
231
|
});
|
|
350
232
|
}
|
|
351
|
-
return new Response("Solard price feed\n");
|
|
233
|
+
return new Response("Solard trader price feed\n");
|
|
352
234
|
},
|
|
353
235
|
websocket: {
|
|
354
236
|
open(socket) {
|
|
355
|
-
clients.set(socket, {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
allPrices: false,
|
|
237
|
+
clients.set(socket, { mints: new Set() });
|
|
238
|
+
status(socket, "ready", {
|
|
239
|
+
upstreamMints: refs.size,
|
|
359
240
|
});
|
|
360
|
-
send(socket, { type: "status", atMs: Date.now(), event: "ready" });
|
|
361
241
|
},
|
|
362
242
|
message(socket, message) {
|
|
243
|
+
let command: PriceFeedCommand;
|
|
363
244
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
});
|
|
374
|
-
});
|
|
245
|
+
command = JSON.parse(String(message)) as PriceFeedCommand;
|
|
246
|
+
if (
|
|
247
|
+
!command ||
|
|
248
|
+
(command.op !== "subscribe" &&
|
|
249
|
+
command.op !== "unsubscribe" &&
|
|
250
|
+
command.op !== "ping")
|
|
251
|
+
) {
|
|
252
|
+
throw new Error("op must be subscribe, unsubscribe, or ping");
|
|
253
|
+
}
|
|
375
254
|
} catch (error) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
atMs: Date.now(),
|
|
379
|
-
event: "client-error",
|
|
380
|
-
data: { error: errorText(error) },
|
|
381
|
-
});
|
|
255
|
+
status(socket, "client-error", { error: errorText(error) });
|
|
256
|
+
return;
|
|
382
257
|
}
|
|
258
|
+
commandQueue = commandQueue
|
|
259
|
+
.then(() => applyCommand(socket, command))
|
|
260
|
+
.catch((error) => {
|
|
261
|
+
status(socket, "client-error", { error: errorText(error) });
|
|
262
|
+
});
|
|
383
263
|
},
|
|
384
264
|
close(socket) {
|
|
385
|
-
|
|
265
|
+
commandQueue = commandQueue
|
|
266
|
+
.then(() => releaseClient(socket))
|
|
267
|
+
.catch((error) => {
|
|
268
|
+
report("release-client-error", { error: errorText(error) });
|
|
269
|
+
});
|
|
386
270
|
},
|
|
387
271
|
},
|
|
388
272
|
});
|
|
@@ -391,16 +275,11 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
391
275
|
() =>
|
|
392
276
|
report("heartbeat", {
|
|
393
277
|
clients: clients.size,
|
|
394
|
-
|
|
278
|
+
upstreamMints: refs.size,
|
|
279
|
+
watchedTokens: tradeSubscription?.list().length ?? 0,
|
|
280
|
+
cachedPrices: latest.size,
|
|
395
281
|
tradeCount,
|
|
396
|
-
|
|
397
|
-
cachedLaunches: launches.size,
|
|
398
|
-
latestPrices: latest.size,
|
|
399
|
-
discoveredMints: discoveredMints.size,
|
|
400
|
-
explicitMints: explicitMintRefs.size,
|
|
401
|
-
allPriceClients,
|
|
402
|
-
watchedTokens: tradeSubscription?.listTokens().length ?? 0,
|
|
403
|
-
solUsd,
|
|
282
|
+
subscriptions: Object.fromEntries(refs),
|
|
404
283
|
}),
|
|
405
284
|
Math.max(1_000, Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000))),
|
|
406
285
|
);
|
|
@@ -408,9 +287,6 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
408
287
|
report("ready", {
|
|
409
288
|
url: `ws://${host}:${server.port}/ws`,
|
|
410
289
|
health: `http://${host}:${server.port}/health`,
|
|
411
|
-
rpc: publicEndpoint(rpc),
|
|
412
|
-
ws: ws ? publicEndpoint(ws) : "connection-default",
|
|
413
|
-
includeMayhem,
|
|
414
290
|
});
|
|
415
291
|
|
|
416
292
|
const stop = (): void => {
|
|
@@ -422,19 +298,18 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
422
298
|
process.once("SIGTERM", stop);
|
|
423
299
|
|
|
424
300
|
try {
|
|
425
|
-
await
|
|
301
|
+
await tradeSubscription.closed;
|
|
426
302
|
} finally {
|
|
427
303
|
clearInterval(heartbeat);
|
|
428
304
|
process.removeListener("SIGINT", stop);
|
|
429
305
|
process.removeListener("SIGTERM", stop);
|
|
430
|
-
await launchSubscription.close();
|
|
431
306
|
await tradeSubscription.close();
|
|
307
|
+
await commandQueue.catch(() => undefined);
|
|
432
308
|
server.stop(true);
|
|
433
309
|
report("stopped", {
|
|
434
|
-
launchCount,
|
|
435
|
-
tradeCount,
|
|
436
|
-
priceCount,
|
|
437
310
|
clients: clients.size,
|
|
311
|
+
upstreamMints: refs.size,
|
|
312
|
+
tradeCount,
|
|
438
313
|
});
|
|
439
314
|
}
|
|
440
315
|
}
|
|
@@ -208,14 +208,14 @@ function required(flags: Flags, key: string): string {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function rpcUrl(flags: Flags): string {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
if (flags.has("rpc") || flags.has("ws"))
|
|
212
|
+
throw new Error(
|
|
213
|
+
"Set only RPC_ENDPOINT in the environment. --rpc and --ws are intentionally unsupported.",
|
|
214
|
+
);
|
|
215
|
+
const value = process.env.RPC_ENDPOINT?.trim();
|
|
216
216
|
if (!value)
|
|
217
217
|
throw new Error(
|
|
218
|
-
"strategy run requires RPC_ENDPOINT
|
|
218
|
+
"strategy run requires RPC_ENDPOINT. WebSocket access is derived from the same endpoint.",
|
|
219
219
|
);
|
|
220
220
|
return value;
|
|
221
221
|
}
|