@solard/cli 0.2.29 → 0.2.31

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.
@@ -1,301 +1,301 @@
1
- import { configure, createMeasure } from "measure-fn";
2
- import slrd, {
3
- type LaunchEvent,
4
- type TradeEvent,
5
- type TradeListener,
6
- } from "@solard/sdk";
7
-
8
- type Flags = Map<string, string>;
9
- type Emit = (value: string) => void;
10
- type VenueId = "pump" | "raydium-launchlab";
11
- type WatchState = {
12
- mint: string;
13
- venue: VenueId;
14
- createdAtMs: number;
15
- lastSeenAtMs: number;
16
- supplyUi: number;
17
- qualified: boolean;
18
- athMarketCapUsd: number | null;
19
- lastNotifiedAthUsd: number | null;
20
- };
21
-
22
- function flag(flags: Flags, key: string): string | undefined {
23
- const value = flags.get(key);
24
- return value && value !== "true" ? value : undefined;
25
- }
26
-
27
- function numberFlag(flags: Flags, key: string, fallback: number): number {
28
- const value = flag(flags, key);
29
- if (value == null) return fallback;
30
- const parsed = Number(value);
31
- if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
32
- return parsed;
33
- }
34
-
35
- function duration(value: string | undefined, fallbackMs: number): number {
36
- if (!value) return fallbackMs;
37
- const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(value.trim());
38
- if (!match) throw new Error(`Invalid duration: ${value}`);
39
- const scale = ({ ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const)[
40
- (match[2]?.toLowerCase() ?? "ms") as "ms" | "s" | "m" | "h"
41
- ];
42
- return Math.max(100, Math.floor(Number(match[1]) * scale));
43
- }
44
-
45
- function venueList(flags: Flags): VenueId[] {
46
- const raw = flag(flags, "venue") ?? "pump,raydium-launchlab";
47
- const values = raw
48
- .split(",")
49
- .map((value) => value.trim().toLowerCase())
50
- .filter(Boolean)
51
- .map((value) =>
52
- value === "launchlab" || value === "raydium" || value === "stonkfun"
53
- ? "raydium-launchlab"
54
- : value,
55
- );
56
- const out = [...new Set(values)] as string[];
57
- for (const value of out)
58
- if (value !== "pump" && value !== "raydium-launchlab")
59
- throw new Error(`Unsupported --venue value: ${value}`);
60
- return out as VenueId[];
61
- }
62
-
63
- function requireSingleEndpoint(flags: Flags): void {
64
- if (flags.has("rpc") || flags.has("ws"))
65
- throw new Error(
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.",
71
- );
72
- }
73
-
74
- export async function runLaunchWatchCommand(args: {
75
- flags: Flags;
76
- emit: Emit;
77
- forcedVenues?: VenueId[];
78
- }): Promise<void> {
79
- configure({
80
- silent: false,
81
- logger(_event: unknown, next?: () => void) {
82
- next?.();
83
- },
84
- });
85
- const json = args.flags.has("json");
86
- const m = createMeasure("slrd:launch-watch", { maxResultLength: 1600 });
87
- const report = <T extends Record<string, unknown>>(
88
- label: string,
89
- value: T,
90
- ): T => {
91
- if (json) {
92
- args.emit(`${JSON.stringify({ type: label, ...value })}\n`);
93
- return value;
94
- }
95
- return m.sync(
96
- { start: () => label, end: (result: T) => result },
97
- () => value,
98
- );
99
- };
100
- const venues = args.forcedVenues ?? venueList(args.flags);
101
- const minMarketCapUsd = numberFlag(args.flags, "min-mcap", 5_000);
102
- const athStepPct = numberFlag(args.flags, "ath-step-pct", 20);
103
- if (!(minMarketCapUsd > 0)) throw new Error("--min-mcap must be > 0");
104
- if (!(athStepPct > 0)) throw new Error("--ath-step-pct must be > 0");
105
- const trackTtlMs = duration(flag(args.flags, "track-ttl"), 30 * 60_000);
106
- const showNew = args.flags.has("show-new");
107
- const showPrices = args.flags.has("trades") || args.flags.has("prices");
108
- requireSingleEndpoint(args.flags);
109
- const states = new Map<string, WatchState>();
110
- let launches = 0;
111
- let prices = 0;
112
- let qualified = 0;
113
- let thresholdNotifications = 0;
114
- let athNotifications = 0;
115
- let ignoredMayhem = 0;
116
- let stopped = false;
117
- let tradeSubscription: TradeListener | null = null;
118
-
119
- const onLaunch = (event: LaunchEvent) => {
120
- if (!venues.includes(event.venue)) return;
121
- if (event.isMayhemMode === true) {
122
- ignoredMayhem += 1;
123
- return;
124
- }
125
- states.set(event.mint, {
126
- mint: event.mint,
127
- venue: event.venue,
128
- createdAtMs: event.atMs,
129
- lastSeenAtMs: event.atMs,
130
- supplyUi: event.supplyUi,
131
- qualified: false,
132
- athMarketCapUsd: null,
133
- lastNotifiedAthUsd: null,
134
- });
135
- void tradeSubscription?.add(event.mint);
136
- launches += 1;
137
- if (showNew)
138
- report("new launch", {
139
- at: new Date(event.atMs).toISOString(),
140
- venue: event.venue,
141
- mint: event.mint,
142
- symbol: event.symbol,
143
- name: event.name,
144
- supplyUi: event.supplyUi,
145
- });
146
- };
147
-
148
- const onTrade = (event: TradeEvent) => {
149
- const state = states.get(event.mint);
150
- if (!state) return;
151
- state.lastSeenAtMs = event.atMs;
152
- const priceSol = event.market.priceSol;
153
- const priceUsd = event.market.priceUsd;
154
- const marketCapUsd = event.market.marketCapUsd;
155
- prices += 1;
156
- if (showPrices)
157
- report("price", {
158
- at: new Date(event.atMs).toISOString(),
159
- venue: event.venue,
160
- mint: event.mint,
161
- marketCapUsd,
162
- priceUsd,
163
- priceSol,
164
- source: `${event.venue}-trade`,
165
- });
166
- if (!(
167
- marketCapUsd != null &&
168
- Number.isFinite(marketCapUsd) &&
169
- marketCapUsd > 0
170
- ))
171
- return;
172
- state.athMarketCapUsd = Math.max(state.athMarketCapUsd ?? 0, marketCapUsd);
173
- if (!state.qualified) {
174
- if (marketCapUsd < minMarketCapUsd) return;
175
- state.qualified = true;
176
- state.lastNotifiedAthUsd = marketCapUsd;
177
- qualified += 1;
178
- thresholdNotifications += 1;
179
- const nextAthNotifyUsd = marketCapUsd * (1 + athStepPct / 100);
180
- if (!args.flags.has("no-bell") && !json) process.stderr.write("\x07");
181
- report("market cap threshold", {
182
- at: new Date(event.atMs).toISOString(),
183
- venue: state.venue,
184
- mint: event.mint,
185
- marketCapUsd,
186
- thresholdMarketCapUsd: minMarketCapUsd,
187
- nextAthNotifyUsd,
188
- athStepPct,
189
- priceUsd,
190
- priceSol,
191
- signature: event.signature,
192
- });
193
- return;
194
- }
195
- const previous = state.lastNotifiedAthUsd;
196
- if (!(previous != null && previous > 0)) {
197
- state.lastNotifiedAthUsd = marketCapUsd;
198
- return;
199
- }
200
- const target = previous * (1 + athStepPct / 100);
201
- if (marketCapUsd + 1e-12 < target) return;
202
- state.lastNotifiedAthUsd = marketCapUsd;
203
- athNotifications += 1;
204
- const increasePct = (marketCapUsd / previous - 1) * 100;
205
- const nextAthNotifyUsd = marketCapUsd * (1 + athStepPct / 100);
206
- if (!args.flags.has("no-bell") && !json) process.stderr.write("\x07");
207
- report("new ath", {
208
- at: new Date(event.atMs).toISOString(),
209
- venue: state.venue,
210
- mint: event.mint,
211
- marketCapUsd,
212
- previousNotifiedAthUsd: previous,
213
- nextAthNotifyUsd,
214
- athStepPct,
215
- increasePct,
216
- priceUsd,
217
- priceSol,
218
- signature: event.signature,
219
- });
220
- };
221
-
222
- const controller = new AbortController();
223
- tradeSubscription = await slrd.listenTrades({
224
- tokens: [],
225
- venues: [
226
- ...(venues.includes("pump") ? (["pump", "pumpswap"] as const) : []),
227
- ...(venues.includes("raydium-launchlab")
228
- ? (["raydium-launchlab"] as const)
229
- : []),
230
- ],
231
- signal: controller.signal,
232
- onStatus(event, data) {
233
- report(`trades ${event}`, data ?? {});
234
- },
235
- });
236
- tradeSubscription.onTrade(onTrade);
237
- const launchSubscription = await slrd.subscribeLaunches({
238
- venues,
239
- signal: controller.signal,
240
- onLaunch,
241
- onStatus(event, data) {
242
- report(`launches ${event}`, data ?? {});
243
- },
244
- });
245
-
246
- const stop = () => {
247
- stopped = true;
248
- controller.abort();
249
- };
250
- process.once("SIGINT", stop);
251
- process.once("SIGTERM", stop);
252
- report("ready", {
253
- rpc: "RPC_ENDPOINT",
254
- websocket: "derived-from-RPC_ENDPOINT",
255
- venues,
256
- minMarketCapUsd,
257
- athStepPct,
258
- excludeMayhem: true,
259
- trackTtlMs,
260
- upstreamSubscriptions: venues.length,
261
- });
262
- const heartbeat = setInterval(
263
- () => {
264
- const now = Date.now();
265
- for (const [mint, state] of states) {
266
- if (!state.qualified && now - state.lastSeenAtMs > trackTtlMs) {
267
- states.delete(mint);
268
- void tradeSubscription?.remove(mint);
269
- }
270
- }
271
- report("heartbeat", {
272
- launches,
273
- ignoredMayhem,
274
- tracked: states.size,
275
- qualified,
276
- prices,
277
- thresholdNotifications,
278
- athNotifications,
279
- watchedTokens: tradeSubscription?.list().length ?? 0,
280
- });
281
- },
282
- Math.max(1_000, Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000))),
283
- );
284
-
285
- try {
286
- while (!stopped) await new Promise((resolve) => setTimeout(resolve, 250));
287
- } finally {
288
- clearInterval(heartbeat);
289
- controller.abort();
290
- await launchSubscription.close();
291
- await tradeSubscription.close();
292
- process.removeListener("SIGINT", stop);
293
- process.removeListener("SIGTERM", stop);
294
- report("stopped", {
295
- launches,
296
- prices,
297
- thresholdNotifications,
298
- athNotifications,
299
- });
300
- }
301
- }
1
+ import { configure, createMeasure } from "measure-fn";
2
+ import slrd, {
3
+ type LaunchEvent,
4
+ type TradeEvent,
5
+ type TradeListener,
6
+ } from "@solard/sdk";
7
+
8
+ type Flags = Map<string, string>;
9
+ type Emit = (value: string) => void;
10
+ type VenueId = "pump" | "raydium-launchlab";
11
+ type WatchState = {
12
+ mint: string;
13
+ venue: VenueId;
14
+ createdAtMs: number;
15
+ lastSeenAtMs: number;
16
+ supplyUi: number;
17
+ qualified: boolean;
18
+ athMarketCapUsd: number | null;
19
+ lastNotifiedAthUsd: number | null;
20
+ };
21
+
22
+ function flag(flags: Flags, key: string): string | undefined {
23
+ const value = flags.get(key);
24
+ return value && value !== "true" ? value : undefined;
25
+ }
26
+
27
+ function numberFlag(flags: Flags, key: string, fallback: number): number {
28
+ const value = flag(flags, key);
29
+ if (value == null) return fallback;
30
+ const parsed = Number(value);
31
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
32
+ return parsed;
33
+ }
34
+
35
+ function duration(value: string | undefined, fallbackMs: number): number {
36
+ if (!value) return fallbackMs;
37
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(value.trim());
38
+ if (!match) throw new Error(`Invalid duration: ${value}`);
39
+ const scale = ({ ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const)[
40
+ (match[2]?.toLowerCase() ?? "ms") as "ms" | "s" | "m" | "h"
41
+ ];
42
+ return Math.max(100, Math.floor(Number(match[1]) * scale));
43
+ }
44
+
45
+ function venueList(flags: Flags): VenueId[] {
46
+ const raw = flag(flags, "venue") ?? "pump,raydium-launchlab";
47
+ const values = raw
48
+ .split(",")
49
+ .map((value) => value.trim().toLowerCase())
50
+ .filter(Boolean)
51
+ .map((value) =>
52
+ value === "launchlab" || value === "raydium" || value === "stonkfun"
53
+ ? "raydium-launchlab"
54
+ : value,
55
+ );
56
+ const out = [...new Set(values)] as string[];
57
+ for (const value of out)
58
+ if (value !== "pump" && value !== "raydium-launchlab")
59
+ throw new Error(`Unsupported --venue value: ${value}`);
60
+ return out as VenueId[];
61
+ }
62
+
63
+ function requireSingleEndpoint(flags: Flags): void {
64
+ if (flags.has("rpc") || flags.has("ws"))
65
+ throw new Error(
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.",
71
+ );
72
+ }
73
+
74
+ export async function runLaunchWatchCommand(args: {
75
+ flags: Flags;
76
+ emit: Emit;
77
+ forcedVenues?: VenueId[];
78
+ }): Promise<void> {
79
+ configure({
80
+ silent: false,
81
+ logger(_event: unknown, next?: () => void) {
82
+ next?.();
83
+ },
84
+ });
85
+ const json = args.flags.has("json");
86
+ const m = createMeasure("slrd:launch-watch", { maxResultLength: 1600 });
87
+ const report = <T extends Record<string, unknown>>(
88
+ label: string,
89
+ value: T,
90
+ ): T => {
91
+ if (json) {
92
+ args.emit(`${JSON.stringify({ type: label, ...value })}\n`);
93
+ return value;
94
+ }
95
+ return m.sync(
96
+ { start: () => label, end: (result: T) => result },
97
+ () => value,
98
+ );
99
+ };
100
+ const venues = args.forcedVenues ?? venueList(args.flags);
101
+ const minMarketCapUsd = numberFlag(args.flags, "min-mcap", 5_000);
102
+ const athStepPct = numberFlag(args.flags, "ath-step-pct", 20);
103
+ if (!(minMarketCapUsd > 0)) throw new Error("--min-mcap must be > 0");
104
+ if (!(athStepPct > 0)) throw new Error("--ath-step-pct must be > 0");
105
+ const trackTtlMs = duration(flag(args.flags, "track-ttl"), 30 * 60_000);
106
+ const showNew = args.flags.has("show-new");
107
+ const showPrices = args.flags.has("trades") || args.flags.has("prices");
108
+ requireSingleEndpoint(args.flags);
109
+ const states = new Map<string, WatchState>();
110
+ let launches = 0;
111
+ let prices = 0;
112
+ let qualified = 0;
113
+ let thresholdNotifications = 0;
114
+ let athNotifications = 0;
115
+ let ignoredMayhem = 0;
116
+ let stopped = false;
117
+ let tradeSubscription: TradeListener | null = null;
118
+
119
+ const onLaunch = (event: LaunchEvent) => {
120
+ if (!venues.includes(event.venue)) return;
121
+ if (event.isMayhemMode === true) {
122
+ ignoredMayhem += 1;
123
+ return;
124
+ }
125
+ states.set(event.mint, {
126
+ mint: event.mint,
127
+ venue: event.venue,
128
+ createdAtMs: event.atMs,
129
+ lastSeenAtMs: event.atMs,
130
+ supplyUi: event.supplyUi,
131
+ qualified: false,
132
+ athMarketCapUsd: null,
133
+ lastNotifiedAthUsd: null,
134
+ });
135
+ void tradeSubscription?.add(event.mint);
136
+ launches += 1;
137
+ if (showNew)
138
+ report("new launch", {
139
+ at: new Date(event.atMs).toISOString(),
140
+ venue: event.venue,
141
+ mint: event.mint,
142
+ symbol: event.symbol,
143
+ name: event.name,
144
+ supplyUi: event.supplyUi,
145
+ });
146
+ };
147
+
148
+ const onTrade = (event: TradeEvent) => {
149
+ const state = states.get(event.mint);
150
+ if (!state) return;
151
+ state.lastSeenAtMs = event.atMs;
152
+ const priceSol = event.market.priceSol;
153
+ const priceUsd = event.market.priceUsd;
154
+ const marketCapUsd = event.market.marketCapUsd;
155
+ prices += 1;
156
+ if (showPrices)
157
+ report("price", {
158
+ at: new Date(event.atMs).toISOString(),
159
+ venue: event.venue,
160
+ mint: event.mint,
161
+ marketCapUsd,
162
+ priceUsd,
163
+ priceSol,
164
+ source: `${event.venue}-trade`,
165
+ });
166
+ if (!(
167
+ marketCapUsd != null &&
168
+ Number.isFinite(marketCapUsd) &&
169
+ marketCapUsd > 0
170
+ ))
171
+ return;
172
+ state.athMarketCapUsd = Math.max(state.athMarketCapUsd ?? 0, marketCapUsd);
173
+ if (!state.qualified) {
174
+ if (marketCapUsd < minMarketCapUsd) return;
175
+ state.qualified = true;
176
+ state.lastNotifiedAthUsd = marketCapUsd;
177
+ qualified += 1;
178
+ thresholdNotifications += 1;
179
+ const nextAthNotifyUsd = marketCapUsd * (1 + athStepPct / 100);
180
+ if (!args.flags.has("no-bell") && !json) process.stderr.write("\x07");
181
+ report("market cap threshold", {
182
+ at: new Date(event.atMs).toISOString(),
183
+ venue: state.venue,
184
+ mint: event.mint,
185
+ marketCapUsd,
186
+ thresholdMarketCapUsd: minMarketCapUsd,
187
+ nextAthNotifyUsd,
188
+ athStepPct,
189
+ priceUsd,
190
+ priceSol,
191
+ signature: event.signature,
192
+ });
193
+ return;
194
+ }
195
+ const previous = state.lastNotifiedAthUsd;
196
+ if (!(previous != null && previous > 0)) {
197
+ state.lastNotifiedAthUsd = marketCapUsd;
198
+ return;
199
+ }
200
+ const target = previous * (1 + athStepPct / 100);
201
+ if (marketCapUsd + 1e-12 < target) return;
202
+ state.lastNotifiedAthUsd = marketCapUsd;
203
+ athNotifications += 1;
204
+ const increasePct = (marketCapUsd / previous - 1) * 100;
205
+ const nextAthNotifyUsd = marketCapUsd * (1 + athStepPct / 100);
206
+ if (!args.flags.has("no-bell") && !json) process.stderr.write("\x07");
207
+ report("new ath", {
208
+ at: new Date(event.atMs).toISOString(),
209
+ venue: state.venue,
210
+ mint: event.mint,
211
+ marketCapUsd,
212
+ previousNotifiedAthUsd: previous,
213
+ nextAthNotifyUsd,
214
+ athStepPct,
215
+ increasePct,
216
+ priceUsd,
217
+ priceSol,
218
+ signature: event.signature,
219
+ });
220
+ };
221
+
222
+ const controller = new AbortController();
223
+ tradeSubscription = await slrd.listenTrades({
224
+ tokens: [],
225
+ venues: [
226
+ ...(venues.includes("pump") ? (["pump", "pumpswap"] as const) : []),
227
+ ...(venues.includes("raydium-launchlab")
228
+ ? (["raydium-launchlab"] as const)
229
+ : []),
230
+ ],
231
+ signal: controller.signal,
232
+ onStatus(event, data) {
233
+ report(`trades ${event}`, data ?? {});
234
+ },
235
+ });
236
+ tradeSubscription.onTrade(onTrade);
237
+ const launchSubscription = await slrd.subscribeLaunches({
238
+ venues,
239
+ signal: controller.signal,
240
+ onLaunch,
241
+ onStatus(event, data) {
242
+ report(`launches ${event}`, data ?? {});
243
+ },
244
+ });
245
+
246
+ const stop = () => {
247
+ stopped = true;
248
+ controller.abort();
249
+ };
250
+ process.once("SIGINT", stop);
251
+ process.once("SIGTERM", stop);
252
+ report("ready", {
253
+ rpc: "RPC_ENDPOINT",
254
+ websocket: "derived-from-RPC_ENDPOINT",
255
+ venues,
256
+ minMarketCapUsd,
257
+ athStepPct,
258
+ excludeMayhem: true,
259
+ trackTtlMs,
260
+ upstreamSubscriptions: venues.length,
261
+ });
262
+ const heartbeat = setInterval(
263
+ () => {
264
+ const now = Date.now();
265
+ for (const [mint, state] of states) {
266
+ if (!state.qualified && now - state.lastSeenAtMs > trackTtlMs) {
267
+ states.delete(mint);
268
+ void tradeSubscription?.remove(mint);
269
+ }
270
+ }
271
+ report("heartbeat", {
272
+ launches,
273
+ ignoredMayhem,
274
+ tracked: states.size,
275
+ qualified,
276
+ prices,
277
+ thresholdNotifications,
278
+ athNotifications,
279
+ watchedTokens: tradeSubscription?.list().length ?? 0,
280
+ });
281
+ },
282
+ Math.max(1_000, Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000))),
283
+ );
284
+
285
+ try {
286
+ while (!stopped) await new Promise((resolve) => setTimeout(resolve, 250));
287
+ } finally {
288
+ clearInterval(heartbeat);
289
+ controller.abort();
290
+ await launchSubscription.close();
291
+ await tradeSubscription.close();
292
+ process.removeListener("SIGINT", stop);
293
+ process.removeListener("SIGTERM", stop);
294
+ report("stopped", {
295
+ launches,
296
+ prices,
297
+ thresholdNotifications,
298
+ athNotifications,
299
+ });
300
+ }
301
+ }