@solard/cli 0.1.8 → 0.2.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.
@@ -0,0 +1,1652 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ import { Keypair, PublicKey } from "@solana/web3.js";
5
+ import {
6
+ abortArmedBuyerEndpoints,
7
+ assertArmedBuyerEndpointsReady,
8
+ bondingCurvePda,
9
+ createTraderSolard,
10
+ executePumpTokenLaunch,
11
+ generateMintKeypairWithSuffix,
12
+ installPumpLaunchSenders,
13
+ loadExplicitBuyerAllocations,
14
+ loadGroupBuyerAllocations,
15
+ normalizeTraderSubmitMode,
16
+ parseArmedBuyerEndpoint,
17
+ PUMP_PROGRAM_ID,
18
+ preparePumpTokenLaunch,
19
+ pumpLaunchEnvironment,
20
+ releaseArmedBuyerEndpoints,
21
+ sol,
22
+ TOKEN_2022_ID,
23
+ uploadPumpMetadata,
24
+ usesHeliusSenderForLaunch,
25
+ validateHeliusTip,
26
+ validateJitoTip,
27
+ type ExplicitBuyerPlanRow,
28
+ type LaunchReporter,
29
+ type MetadataUploaderId,
30
+ type PumpLaunchEnvironment,
31
+ type PumpTokenLaunchPlan,
32
+ type PumpTokenLaunchResult,
33
+ type SendReceipt,
34
+ type SimulationResult,
35
+ type Solard,
36
+ type TokenMetadata,
37
+ type TraderSubmitMode,
38
+ } from "@solard/sdk";
39
+
40
+ export type Flags = Map<string, string[]>;
41
+
42
+ export type PumpTokenMetadataInput = {
43
+ alias: string;
44
+ name: string;
45
+ symbol: string;
46
+ uri?: string;
47
+ imagePath?: string;
48
+ description?: string;
49
+ website?: string;
50
+ twitter?: string;
51
+ telegram?: string;
52
+ video?: string;
53
+ showName?: boolean;
54
+ };
55
+
56
+ export type PumpTokenLaunchCliOptions = {
57
+ /** Safe default: do not start trader spam until the deploy signature is processed. */
58
+ defaultSubmitMode?: TraderSubmitMode;
59
+ defaultDeploymentPriorityMicroLamports?: number;
60
+ defaultBuyerPriorityMicroLamports?: number;
61
+ defaultSlippageBps?: number;
62
+ /** Persist the token after any live execution attempt, even if some buyers fail. */
63
+ persistOnLive?: boolean;
64
+ report?: LaunchReporter;
65
+ };
66
+
67
+ export type PumpTokenLaunchCliResult = {
68
+ createdAt: string;
69
+ live: boolean;
70
+ creator: string;
71
+ buyerGroup: string | null;
72
+ buyPlan?: string | null;
73
+ transport: Record<string, unknown>;
74
+ token: PumpTokenMetadataInput & {
75
+ uri: string;
76
+ metadataUri: string;
77
+ mint: string;
78
+ feeMode: "creator-fees";
79
+ cashback: boolean;
80
+ mayhemMode: boolean;
81
+ vanityMintSuffix?: string | null;
82
+ vanityMintAttempts?: number | null;
83
+ vanityMintElapsedMs?: number | null;
84
+ };
85
+ result: PumpTokenLaunchResult;
86
+ };
87
+
88
+ export function parseArgs(argv: string[]): {
89
+ flags: Flags;
90
+ positionals: string[];
91
+ } {
92
+ const flags = new Map<string, string[]>();
93
+ const positionals: string[] = [];
94
+
95
+ for (let index = 0; index < argv.length; index += 1) {
96
+ const item = argv[index]!;
97
+ if (!item.startsWith("--")) {
98
+ positionals.push(item);
99
+ continue;
100
+ }
101
+
102
+ const [key, inline] = item.slice(2).split("=", 2);
103
+ let value = inline ?? "true";
104
+ if (
105
+ inline == null &&
106
+ argv[index + 1] &&
107
+ !argv[index + 1]!.startsWith("--")
108
+ ) {
109
+ value = argv[++index]!;
110
+ }
111
+
112
+ const current = flags.get(key!) ?? [];
113
+ current.push(value);
114
+ flags.set(key!, current);
115
+ }
116
+
117
+ return { flags, positionals };
118
+ }
119
+
120
+ export function first(
121
+ flags: Flags,
122
+ key: string,
123
+ env?: string,
124
+ ): string | undefined {
125
+ return flags.get(key)?.at(-1) ?? (env ? process.env[env] : undefined);
126
+ }
127
+
128
+ function envFirst(...names: string[]): string | undefined {
129
+ for (const name of names) {
130
+ const value = process.env[name]?.trim();
131
+ if (value) return value;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ export function required(flags: Flags, key: string, env?: string): string {
137
+ const value = first(flags, key, env);
138
+ if (!value || value === "true") {
139
+ throw new Error(`Missing --${key} <value>${env ? ` or ${env}` : ""}`);
140
+ }
141
+ return value;
142
+ }
143
+
144
+ export function enabled(flags: Flags, key: string, env?: string): boolean {
145
+ const value = first(flags, key, env);
146
+ return value === "true" || value === "1";
147
+ }
148
+
149
+ export function numberFlag(
150
+ flags: Flags,
151
+ key: string,
152
+ fallback: number,
153
+ env?: string,
154
+ ): number {
155
+ const raw = first(flags, key, env);
156
+ if (raw == null) return fallback;
157
+ const parsed = Number(raw);
158
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${raw}`);
159
+ return parsed;
160
+ }
161
+
162
+ export function bigintFlag(
163
+ flags: Flags,
164
+ key: string,
165
+ fallback: bigint,
166
+ env?: string,
167
+ ): bigint {
168
+ const raw = first(flags, key, env);
169
+ if (raw == null) return fallback;
170
+ try {
171
+ return BigInt(raw);
172
+ } catch {
173
+ throw new Error(`Invalid --${key}: ${raw}`);
174
+ }
175
+ }
176
+
177
+ export function json(value: unknown): string {
178
+ return JSON.stringify(
179
+ value,
180
+ (_key, item) => (typeof item === "bigint" ? item.toString() : item),
181
+ 2,
182
+ );
183
+ }
184
+
185
+ function defaultReport(label: string, value: unknown): void {
186
+ console.log(`${label}: ${json(value)}`);
187
+ }
188
+
189
+ function nonEmpty(value: string | undefined): string | undefined {
190
+ const cleaned = value?.trim();
191
+ return cleaned ? cleaned : undefined;
192
+ }
193
+
194
+ function optionalBoolean(value: unknown): boolean | undefined {
195
+ if (typeof value === "boolean") return value;
196
+ if (typeof value !== "string") return undefined;
197
+
198
+ switch (value.trim().toLowerCase()) {
199
+ case "true":
200
+ case "1":
201
+ case "yes":
202
+ return true;
203
+ case "false":
204
+ case "0":
205
+ case "no":
206
+ return false;
207
+ default:
208
+ return undefined;
209
+ }
210
+ }
211
+
212
+ type LoadedMintKeypair = {
213
+ mint: Keypair;
214
+ path: string;
215
+ address: string;
216
+ };
217
+
218
+ function mintSecretKeyBytes(value: unknown): Uint8Array {
219
+ const source = Array.isArray(value)
220
+ ? value
221
+ : value &&
222
+ typeof value === "object" &&
223
+ Array.isArray((value as { secretKey?: unknown }).secretKey)
224
+ ? (value as { secretKey: unknown[] }).secretKey
225
+ : null;
226
+
227
+ if (!source || source.length !== 64) {
228
+ throw new Error(
229
+ "Mint keypair JSON must be an array of exactly 64 bytes, or an object with a 64-byte secretKey array.",
230
+ );
231
+ }
232
+
233
+ return Uint8Array.from(
234
+ source.map((item, index) => {
235
+ const byte = Number(item);
236
+ if (!Number.isInteger(byte) || byte < 0 || byte > 255) {
237
+ throw new Error(`Invalid mint keypair byte at index ${index}.`);
238
+ }
239
+ return byte;
240
+ }),
241
+ );
242
+ }
243
+
244
+ export function loadMintKeypairFromFlags(
245
+ flags: Flags,
246
+ requiredSuffix: string | null,
247
+ ): LoadedMintKeypair | null {
248
+ const configuredPath = nonEmpty(first(flags, "mint-keypair"));
249
+ const expectedAddress = nonEmpty(first(flags, "mint-address"));
250
+
251
+ if (expectedAddress && !configuredPath) {
252
+ throw new Error(
253
+ "--mint-address cannot be used without --mint-keypair; the mint keypair must sign token creation.",
254
+ );
255
+ }
256
+
257
+ if (!configuredPath) return null;
258
+
259
+ const absolutePath = resolve(configuredPath);
260
+
261
+ let parsed: unknown;
262
+ try {
263
+ parsed = JSON.parse(readFileSync(absolutePath, "utf8")) as unknown;
264
+ } catch (error) {
265
+ throw new Error(`Could not read mint keypair JSON: ${absolutePath}`, {
266
+ cause: error,
267
+ });
268
+ }
269
+
270
+ const mint = Keypair.fromSecretKey(mintSecretKeyBytes(parsed));
271
+ const address = mint.publicKey.toBase58();
272
+
273
+ if (expectedAddress && address !== expectedAddress) {
274
+ throw new Error(
275
+ `Mint keypair derives ${address}, not the expected --mint-address ${expectedAddress}.`,
276
+ );
277
+ }
278
+
279
+ if (requiredSuffix && !address.endsWith(requiredSuffix)) {
280
+ throw new Error(
281
+ `Mint keypair address ${address} does not end with required suffix ${requiredSuffix}.`,
282
+ );
283
+ }
284
+
285
+ return {
286
+ mint,
287
+ path: absolutePath,
288
+ address,
289
+ };
290
+ }
291
+
292
+ export function pumpTokenMetadataInput(flags: Flags): PumpTokenMetadataInput {
293
+ const metadataPath = first(flags, "metadata");
294
+ let loaded: Partial<PumpTokenMetadataInput> = {};
295
+ let baseDir = process.cwd();
296
+
297
+ if (metadataPath) {
298
+ const absolute = resolve(metadataPath);
299
+ baseDir = dirname(absolute);
300
+ loaded = JSON.parse(
301
+ readFileSync(absolute, "utf8"),
302
+ ) as Partial<PumpTokenMetadataInput>;
303
+ }
304
+
305
+ const alias =
306
+ nonEmpty(first(flags, "alias")) ??
307
+ nonEmpty(loaded.alias) ??
308
+ required(flags, "alias");
309
+ const name =
310
+ nonEmpty(first(flags, "name")) ??
311
+ nonEmpty(loaded.name) ??
312
+ required(flags, "name");
313
+ const symbol =
314
+ nonEmpty(first(flags, "symbol")) ??
315
+ nonEmpty(loaded.symbol) ??
316
+ required(flags, "symbol");
317
+ const configuredImage =
318
+ nonEmpty(first(flags, "image")) ?? nonEmpty(loaded.imagePath);
319
+ const showName = flags.has("hide-name")
320
+ ? false
321
+ : (optionalBoolean(first(flags, "show-name")) ??
322
+ optionalBoolean(loaded.showName));
323
+
324
+ return {
325
+ alias,
326
+ name,
327
+ symbol,
328
+ uri: nonEmpty(first(flags, "uri")) ?? nonEmpty(loaded.uri),
329
+ imagePath: configuredImage ? resolve(baseDir, configuredImage) : undefined,
330
+ description:
331
+ nonEmpty(first(flags, "description")) ?? nonEmpty(loaded.description),
332
+ website: nonEmpty(first(flags, "website")) ?? nonEmpty(loaded.website),
333
+ twitter: nonEmpty(first(flags, "twitter")) ?? nonEmpty(loaded.twitter),
334
+ telegram: nonEmpty(first(flags, "telegram")) ?? nonEmpty(loaded.telegram),
335
+ video: nonEmpty(first(flags, "video")) ?? nonEmpty(loaded.video),
336
+ showName,
337
+ };
338
+ }
339
+
340
+ async function assertPumpMintIsUnused(args: {
341
+ slrd: Solard;
342
+ mint: PublicKey;
343
+ report: LaunchReporter;
344
+ }): Promise<void> {
345
+ const bondingCurve = bondingCurvePda(args.mint);
346
+ const [mintAccount, bondingCurveAccount] = await args.slrd
347
+ .connection()
348
+ .getMultipleAccountsInfo([args.mint, bondingCurve], {
349
+ commitment: "confirmed",
350
+ });
351
+
352
+ args.report("pump mint on-chain preflight", {
353
+ mint: args.mint.toBase58(),
354
+ mintExists: mintAccount != null,
355
+ mintOwner: mintAccount?.owner.toBase58() ?? null,
356
+ mintLamports: mintAccount?.lamports ?? null,
357
+ mintDataLength: mintAccount?.data.length ?? null,
358
+ mintIsToken2022: mintAccount?.owner.equals(TOKEN_2022_ID) ?? false,
359
+ bondingCurve: bondingCurve.toBase58(),
360
+ bondingCurveExists: bondingCurveAccount != null,
361
+ bondingCurveOwner: bondingCurveAccount?.owner.toBase58() ?? null,
362
+ bondingCurveIsPump:
363
+ bondingCurveAccount?.owner.equals(PUMP_PROGRAM_ID) ?? false,
364
+ });
365
+
366
+ if (mintAccount == null && bondingCurveAccount == null) return;
367
+
368
+ const state =
369
+ mintAccount != null && bondingCurveAccount != null
370
+ ? "the mint and Pump bonding curve already exist"
371
+ : mintAccount != null
372
+ ? "the mint account is already initialized"
373
+ : "the Pump bonding-curve account already exists";
374
+
375
+ throw new Error(
376
+ `Configured mint ${args.mint.toBase58()} cannot be used for create_v2: ` +
377
+ `${state}. Pump create_v2 requires a new Token-2022 mint account. ` +
378
+ `Run .\\rotate-pump-mint.ps1 to generate a fresh vanity mint and ` +
379
+ `update the saved launch profile.`,
380
+ );
381
+ }
382
+
383
+ function optionalSol(
384
+ flags: Flags,
385
+ solKey: string,
386
+ rawKey: string,
387
+ fallback: bigint,
388
+ ): bigint {
389
+ const raw = first(flags, rawKey);
390
+ if (raw != null) return bigintFlag(flags, rawKey, fallback);
391
+ const human = first(flags, solKey);
392
+ return human == null ? fallback : sol(human).raw;
393
+ }
394
+
395
+ function optionalSolOverride(
396
+ flags: Flags,
397
+ solKey: string,
398
+ rawKey: string,
399
+ ): bigint | undefined {
400
+ const raw = first(flags, rawKey);
401
+ if (raw != null) return bigintFlag(flags, rawKey, 0n);
402
+ const human = first(flags, solKey);
403
+ return human == null ? undefined : sol(human).raw;
404
+ }
405
+
406
+ function optionalSolEnv(
407
+ solEnvNames: string[],
408
+ rawEnvNames: string[],
409
+ ): bigint | undefined {
410
+ const raw = envFirst(...rawEnvNames);
411
+ if (raw != null) {
412
+ try {
413
+ return BigInt(raw);
414
+ } catch {
415
+ throw new Error(`Invalid ${rawEnvNames.join("/")}: ${raw}`);
416
+ }
417
+ }
418
+
419
+ const human = envFirst(...solEnvNames);
420
+ return human == null ? undefined : sol(human).raw;
421
+ }
422
+
423
+ type JitoTipFloorPercentile = "25" | "50" | "75" | "95" | "99" | "ema50";
424
+
425
+ type JitoTipFloorRow = {
426
+ time?: string;
427
+ landed_tips_25th_percentile?: number;
428
+ landed_tips_50th_percentile?: number;
429
+ landed_tips_75th_percentile?: number;
430
+ landed_tips_95th_percentile?: number;
431
+ landed_tips_99th_percentile?: number;
432
+ ema_landed_tips_50th_percentile?: number;
433
+ };
434
+
435
+ type JitoTipSelection = {
436
+ mode: "fixed" | "dynamic";
437
+ source: "configured" | "minimum-fallback" | "tip-floor-rest";
438
+ lamports: bigint;
439
+ percentile?: JitoTipFloorPercentile;
440
+ multiplier?: number;
441
+ rawSol?: number;
442
+ selectedSol: number;
443
+ sampleTime?: string;
444
+ sampleAgeMs?: number;
445
+ endpoint?: string;
446
+ fallbackReason?: string;
447
+ };
448
+
449
+ function jitoTipPercentile(flags: Flags): JitoTipFloorPercentile {
450
+ const value = (
451
+ first(flags, "jito-tip-percentile") ??
452
+ envFirst("JITO_TIP_PERCENTILE") ??
453
+ "95"
454
+ )
455
+ .trim()
456
+ .toLowerCase();
457
+ if (
458
+ value === "25" ||
459
+ value === "50" ||
460
+ value === "75" ||
461
+ value === "95" ||
462
+ value === "99" ||
463
+ value === "ema50"
464
+ ) {
465
+ return value;
466
+ }
467
+ throw new Error(
468
+ `Invalid --jito-tip-percentile: ${value}. Expected 25, 50, 75, 95, 99, or ema50.`,
469
+ );
470
+ }
471
+
472
+ function jitoTipField(
473
+ percentile: JitoTipFloorPercentile,
474
+ ): keyof JitoTipFloorRow {
475
+ switch (percentile) {
476
+ case "25":
477
+ return "landed_tips_25th_percentile";
478
+ case "50":
479
+ return "landed_tips_50th_percentile";
480
+ case "75":
481
+ return "landed_tips_75th_percentile";
482
+ case "95":
483
+ return "landed_tips_95th_percentile";
484
+ case "99":
485
+ return "landed_tips_99th_percentile";
486
+ case "ema50":
487
+ return "ema_landed_tips_50th_percentile";
488
+ }
489
+ }
490
+
491
+ function clampBigInt(value: bigint, minimum: bigint, maximum: bigint): bigint {
492
+ if (value < minimum) return minimum;
493
+ if (value > maximum) return maximum;
494
+ return value;
495
+ }
496
+
497
+ async function resolveJitoBundleTip(args: {
498
+ flags: Flags;
499
+ configuredLamports: bigint;
500
+ }): Promise<JitoTipSelection> {
501
+ const mode = (
502
+ first(args.flags, "jito-tip-mode") ??
503
+ envFirst("JITO_TIP_MODE") ??
504
+ "dynamic"
505
+ )
506
+ .trim()
507
+ .toLowerCase();
508
+
509
+ if (mode === "fixed") {
510
+ return {
511
+ mode: "fixed",
512
+ source: "configured",
513
+ lamports: args.configuredLamports,
514
+ selectedSol: Number(args.configuredLamports) / 1_000_000_000,
515
+ };
516
+ }
517
+ if (mode !== "dynamic") {
518
+ throw new Error(
519
+ `Invalid --jito-tip-mode: ${mode}. Expected dynamic or fixed.`,
520
+ );
521
+ }
522
+
523
+ const endpoint = (
524
+ first(args.flags, "jito-tip-floor-url") ??
525
+ envFirst("JITO_TIP_FLOOR_URL") ??
526
+ "https://bundles.jito.wtf/api/v1/bundles/tip_floor"
527
+ ).trim();
528
+ const percentile = jitoTipPercentile(args.flags);
529
+ const multiplier = numberFlag(
530
+ args.flags,
531
+ "jito-tip-multiplier",
532
+ Number(envFirst("JITO_TIP_MULTIPLIER") ?? "1.25"),
533
+ );
534
+ if (!Number.isFinite(multiplier) || multiplier <= 0) {
535
+ throw new Error(
536
+ `--jito-tip-multiplier must be greater than zero; got ${multiplier}.`,
537
+ );
538
+ }
539
+
540
+ const minimum =
541
+ optionalSolOverride(
542
+ args.flags,
543
+ "jito-tip-min-sol",
544
+ "jito-tip-min-lamports",
545
+ ) ??
546
+ optionalSolEnv(["JITO_TIP_MIN_SOL"], ["JITO_TIP_MIN_LAMPORTS"]) ??
547
+ 1_000n;
548
+ const maximum =
549
+ optionalSolOverride(
550
+ args.flags,
551
+ "jito-tip-max-sol",
552
+ "jito-tip-max-lamports",
553
+ ) ??
554
+ optionalSolEnv(["JITO_TIP_MAX_SOL"], ["JITO_TIP_MAX_LAMPORTS"]) ??
555
+ 1_000_000_000n;
556
+ if (minimum < 1_000n) {
557
+ throw new Error(
558
+ `Jito dynamic tip minimum must be at least 1000 lamports; got ${minimum}.`,
559
+ );
560
+ }
561
+ if (maximum < minimum) {
562
+ throw new Error(
563
+ `Jito dynamic tip maximum ${maximum} is below minimum ${minimum}.`,
564
+ );
565
+ }
566
+
567
+ const timeoutMs = numberFlag(
568
+ args.flags,
569
+ "jito-tip-floor-timeout-ms",
570
+ Number(envFirst("JITO_TIP_FLOOR_TIMEOUT_MS") ?? "2500"),
571
+ );
572
+ const maximumAgeMs = numberFlag(
573
+ args.flags,
574
+ "jito-tip-floor-max-age-ms",
575
+ Number(envFirst("JITO_TIP_FLOOR_MAX_AGE_MS") ?? "300000"),
576
+ );
577
+ if (timeoutMs <= 0 || maximumAgeMs <= 0) {
578
+ throw new Error("Jito tip floor timeout and maximum age must be positive.");
579
+ }
580
+
581
+ try {
582
+ const controller = new AbortController();
583
+ const timeout = setTimeout(
584
+ () =>
585
+ controller.abort(
586
+ new Error(`Jito tip floor request timed out after ${timeoutMs}ms`),
587
+ ),
588
+ timeoutMs,
589
+ );
590
+
591
+ let response: Response;
592
+ try {
593
+ response = await fetch(endpoint, {
594
+ headers: { accept: "application/json" },
595
+ signal: controller.signal,
596
+ });
597
+ } catch (error) {
598
+ if (controller.signal.aborted) {
599
+ throw new Error(
600
+ `Jito tip floor request timed out after ${timeoutMs}ms`,
601
+ );
602
+ }
603
+ throw error;
604
+ } finally {
605
+ clearTimeout(timeout);
606
+ }
607
+
608
+ if (!response.ok) {
609
+ const detail = (await response.text()).slice(0, 500);
610
+ throw new Error(
611
+ `Jito tip floor request failed with HTTP ${response.status}: ${detail}`,
612
+ );
613
+ }
614
+
615
+ const payload = (await response.json()) as
616
+ JitoTipFloorRow | JitoTipFloorRow[];
617
+ const row = Array.isArray(payload) ? payload[0] : payload;
618
+ if (!row) {
619
+ throw new Error("Jito tip floor response contained no samples.");
620
+ }
621
+
622
+ const sampleTime = String(row.time ?? "");
623
+ const sampleTimestamp = Date.parse(sampleTime);
624
+ if (!Number.isFinite(sampleTimestamp)) {
625
+ throw new Error(
626
+ `Jito tip floor response has an invalid time: ${sampleTime}`,
627
+ );
628
+ }
629
+
630
+ const sampleAgeMs = Math.max(0, Date.now() - sampleTimestamp);
631
+ if (sampleAgeMs > maximumAgeMs) {
632
+ throw new Error(
633
+ `Jito tip floor sample is stale: age=${sampleAgeMs}ms ` +
634
+ `maximum=${maximumAgeMs}ms.`,
635
+ );
636
+ }
637
+
638
+ const rawSol = Number(row[jitoTipField(percentile)]);
639
+ if (!Number.isFinite(rawSol) || rawSol <= 0) {
640
+ throw new Error(
641
+ `Jito tip floor percentile ${percentile} is missing or invalid: ` +
642
+ `${String(rawSol)}`,
643
+ );
644
+ }
645
+
646
+ const calculated = BigInt(Math.ceil(rawSol * 1_000_000_000 * multiplier));
647
+ const lamports = clampBigInt(calculated, minimum, maximum);
648
+ return {
649
+ mode: "dynamic",
650
+ source: "tip-floor-rest",
651
+ lamports,
652
+ percentile,
653
+ multiplier,
654
+ rawSol,
655
+ selectedSol: Number(lamports) / 1_000_000_000,
656
+ sampleTime,
657
+ sampleAgeMs,
658
+ endpoint,
659
+ };
660
+ } catch (error) {
661
+ // A dynamic-tip outage must start the bounded auction at the configured
662
+ // minimum, not at the larger fixed-tip fallback.
663
+ const fallbackLamports = minimum;
664
+ const fallbackReason =
665
+ error instanceof Error ? error.message : String(error);
666
+
667
+ return {
668
+ mode: "dynamic",
669
+ source: "minimum-fallback",
670
+ lamports: fallbackLamports,
671
+ percentile,
672
+ multiplier,
673
+ selectedSol: Number(fallbackLamports) / 1_000_000_000,
674
+ endpoint,
675
+ fallbackReason,
676
+ };
677
+ }
678
+ }
679
+
680
+ function senderFlag(flags: Flags, key: string, fallback: string): string {
681
+ const value = first(flags, key);
682
+ if (!value) return fallback;
683
+ if (value !== "helius-fast" && value !== "helius-rpc" && value !== "jito") {
684
+ throw new Error(
685
+ `Invalid --${key}: ${value}. Expected helius-fast, helius-rpc, or jito.`,
686
+ );
687
+ }
688
+ return value;
689
+ }
690
+
691
+ export function pumpLaunchEnvironmentFromFlags(
692
+ flags: Flags,
693
+ base = pumpLaunchEnvironment(),
694
+ ): PumpLaunchEnvironment {
695
+ const deploymentSender = senderFlag(
696
+ flags,
697
+ "deployment-sender",
698
+ String(base.policy.deploymentSender),
699
+ );
700
+ const buyerSender = first(flags, "buyer-sender");
701
+ if (
702
+ buyerSender &&
703
+ buyerSender !== "helius-fast" &&
704
+ buyerSender !== "helius-rpc" &&
705
+ buyerSender !== "jito"
706
+ ) {
707
+ throw new Error(
708
+ `Invalid --buyer-sender: ${buyerSender}. Expected helius-fast, helius-rpc, or jito.`,
709
+ );
710
+ }
711
+
712
+ const fastTraderSender =
713
+ buyerSender ??
714
+ senderFlag(
715
+ flags,
716
+ "fast-trader-sender",
717
+ String(base.policy.fastTraderSender),
718
+ );
719
+ const rpcTraderSender =
720
+ buyerSender ??
721
+ senderFlag(flags, "rpc-trader-sender", String(base.policy.rpcTraderSender));
722
+ const evolutionSender = senderFlag(
723
+ flags,
724
+ "evolution-sender",
725
+ String(base.policy.evolutionSender),
726
+ );
727
+ const fastTraderCount = flags.has("fast-trader-count")
728
+ ? numberFlag(flags, "fast-trader-count", base.policy.fastTraderCount)
729
+ : buyerSender === "helius-fast"
730
+ ? Number.MAX_SAFE_INTEGER
731
+ : buyerSender === "helius-rpc"
732
+ ? 0
733
+ : base.policy.fastTraderCount;
734
+
735
+ const senderUrl =
736
+ first(flags, "helius-sender-url") ??
737
+ first(flags, "sender-url") ??
738
+ envFirst(
739
+ "HELIUS_SENDER_URL",
740
+ "SOLWAL_HELIUS_SENDER_URL",
741
+ "SLRD_HELIUS_SENDER_URL",
742
+ ) ??
743
+ base.senderUrl;
744
+ const tipAccount =
745
+ first(flags, "helius-tip-account") ??
746
+ first(flags, "fast-tip-account") ??
747
+ envFirst(
748
+ "HELIUS_TIP_ACCOUNT",
749
+ "SOLWAL_HELIUS_TIP_ACCOUNT",
750
+ "SLRD_HELIUS_TIP_ACCOUNT",
751
+ ) ??
752
+ base.policy.fastTip.account;
753
+ const tipLamports =
754
+ optionalSolOverride(flags, "helius-tip-sol", "helius-tip-lamports") ??
755
+ optionalSolOverride(flags, "fast-tip-sol", "fast-tip-lamports") ??
756
+ optionalSolEnv(
757
+ ["HELIUS_TIP_SOL", "SOLWAL_HELIUS_TIP_SOL", "SLRD_HELIUS_TIP_SOL"],
758
+ [
759
+ "HELIUS_TIP_LAMPORTS",
760
+ "SOLWAL_HELIUS_TIP_LAMPORTS",
761
+ "SLRD_HELIUS_TIP_LAMPORTS",
762
+ ],
763
+ ) ??
764
+ base.policy.fastTip.lamports;
765
+
766
+ const jitoUrl =
767
+ first(flags, "jito-block-engine-url") ??
768
+ first(flags, "jito-url") ??
769
+ base.jitoUrl;
770
+ const jitoTipAccount =
771
+ first(flags, "jito-tip-account") ??
772
+ envFirst("JITO_TIP_ACCOUNT") ??
773
+ base.policy.jitoTip.account;
774
+ const jitoTipLamports =
775
+ optionalSolOverride(flags, "jito-tip-sol", "jito-tip-lamports") ??
776
+ optionalSolEnv(["JITO_TIP_SOL"], ["JITO_TIP_LAMPORTS"]) ??
777
+ base.policy.jitoTip.lamports;
778
+
779
+ return {
780
+ ...base,
781
+ rpcUrl: first(flags, "rpc-url") ?? envFirst("RPC_ENDPOINT") ?? base.rpcUrl,
782
+ senderUrl,
783
+ jitoUrl,
784
+ cuLimit: numberFlag(flags, "cu-limit", base.cuLimit),
785
+ policy: {
786
+ ...base.policy,
787
+ deploymentSender,
788
+ evolutionSender,
789
+ fastTraderSender,
790
+ rpcTraderSender,
791
+ fastTraderCount,
792
+ fastTip:
793
+ tipAccount || tipLamports != null
794
+ ? { account: tipAccount, lamports: tipLamports }
795
+ : {},
796
+ jitoTip:
797
+ jitoTipAccount || jitoTipLamports != null
798
+ ? { account: jitoTipAccount, lamports: jitoTipLamports }
799
+ : {},
800
+ },
801
+ };
802
+ }
803
+
804
+ function simulationSummary(
805
+ simulation: SimulationResult,
806
+ ): Record<string, unknown> {
807
+ const result: Record<string, unknown> = {
808
+ success: simulation.success,
809
+ cuUsed: simulation.cuUsed ?? null,
810
+ error: simulation.error ?? null,
811
+ accountChanges: Array.isArray(simulation.accountChanges)
812
+ ? simulation.accountChanges.length
813
+ : undefined,
814
+ tokenChanges: Array.isArray(simulation.tokenChanges)
815
+ ? simulation.tokenChanges.length
816
+ : undefined,
817
+ };
818
+ if (!simulation.success && Array.isArray(simulation.logs))
819
+ result.lastLogs = simulation.logs.slice(-12);
820
+ return result;
821
+ }
822
+
823
+ function receiptSummary(receipt: SendReceipt): Record<string, unknown> {
824
+ return {
825
+ signature: receipt.signature ?? null,
826
+ status: receipt.status ?? null,
827
+ slot: receipt.slot ?? null,
828
+ sender: receipt.sender ?? null,
829
+ error: receipt.error ?? null,
830
+ };
831
+ }
832
+
833
+ function summarizeLaunchResult(result: PumpTokenLaunchResult): unknown {
834
+ if (result.mode === "dry-run") {
835
+ return {
836
+ mode: result.mode,
837
+ launchSimulation: simulationSummary(result.launchSimulation),
838
+ deploymentSender: result.deploymentSender,
839
+ buyers: result.buyers,
840
+ };
841
+ }
842
+
843
+ return {
844
+ mode: result.mode,
845
+ launchReceipt: receiptSummary(result.launchReceipt),
846
+ traderReceipts: result.traderReceipts,
847
+ };
848
+ }
849
+
850
+ function requireSkipSimulationForBlindLiveBuys(
851
+ live: boolean,
852
+ submitMode: TraderSubmitMode,
853
+ skipSimulation: boolean,
854
+ hasBuyerGroup: boolean,
855
+ ): void {
856
+ if (
857
+ live &&
858
+ hasBuyerGroup &&
859
+ (normalizeTraderSubmitMode(submitMode) === "blind-spam-after-submit" ||
860
+ normalizeTraderSubmitMode(submitMode) === "jito-bundle") &&
861
+ !skipSimulation
862
+ ) {
863
+ throw new Error(
864
+ "Blind parallel dependent-buy submission and Jito bundles require --skip-simulation after a reviewed dry run.",
865
+ );
866
+ }
867
+ }
868
+
869
+ async function resolveMetadataUri(
870
+ flags: Flags,
871
+ input: PumpTokenMetadataInput,
872
+ ): Promise<{
873
+ uri: string;
874
+ uploaded: Awaited<ReturnType<typeof uploadPumpMetadata>> | null;
875
+ }> {
876
+ let uri = input.uri;
877
+ let uploaded: Awaited<ReturnType<typeof uploadPumpMetadata>> | null = null;
878
+
879
+ if (!uri && input.imagePath) {
880
+ uploaded = await uploadPumpMetadata(
881
+ {
882
+ imagePath: input.imagePath,
883
+ name: input.name,
884
+ symbol: input.symbol,
885
+ description:
886
+ input.description?.trim() || `${input.name} (${input.symbol})`,
887
+ website: input.website,
888
+ twitter: input.twitter,
889
+ telegram: input.telegram,
890
+ video: input.video,
891
+ showName: input.showName,
892
+ },
893
+ {
894
+ provider: (first(
895
+ flags,
896
+ "metadata-provider",
897
+ "PUMP_METADATA_PROVIDER",
898
+ ) ?? "pump-frontend") as MetadataUploaderId,
899
+ },
900
+ );
901
+ uri = uploaded.metadataUri;
902
+ }
903
+
904
+ if (!uri) {
905
+ throw new Error(
906
+ "Provide --uri <metadata-uri> or --metadata <json> / --image <path>.",
907
+ );
908
+ }
909
+
910
+ return { uri, uploaded };
911
+ }
912
+
913
+ function resolveSubmitMode(
914
+ flags: Flags,
915
+ options: PumpTokenLaunchCliOptions,
916
+ ): TraderSubmitMode {
917
+ return normalizeTraderSubmitMode(
918
+ first(flags, "submit-mode") ??
919
+ process.env.SOLWAL_LAUNCH_SUBMIT_MODE?.trim() ??
920
+ process.env.SLRD_LAUNCH_SUBMIT_MODE?.trim() ??
921
+ options.defaultSubmitMode,
922
+ );
923
+ }
924
+
925
+ export function spamOptionsFromFlags(
926
+ flags: Flags,
927
+ fallback: ReturnType<typeof pumpLaunchEnvironment>["spam"],
928
+ ): ReturnType<typeof pumpLaunchEnvironment>["spam"] {
929
+ return {
930
+ intervalMs: numberFlag(flags, "retry-interval-ms", fallback.intervalMs),
931
+ timeoutMs: numberFlag(flags, "retry-timeout-ms", fallback.timeoutMs),
932
+ maxFailedAttempts: numberFlag(
933
+ flags,
934
+ "max-failed-attempts",
935
+ fallback.maxFailedAttempts,
936
+ ),
937
+ recompileIntervalMs: numberFlag(
938
+ flags,
939
+ "retry-recompile-interval-ms",
940
+ fallback.recompileIntervalMs ?? 750,
941
+ ),
942
+ freshQuoteDelayMs: numberFlag(
943
+ flags,
944
+ "fresh-quote-delay-ms",
945
+ fallback.freshQuoteDelayMs ?? 2_500,
946
+ ),
947
+ blockhashRefreshIntervalMs: numberFlag(
948
+ flags,
949
+ "blockhash-refresh-interval-ms",
950
+ fallback.blockhashRefreshIntervalMs ?? 500,
951
+ ),
952
+ readinessTimeoutMs: numberFlag(
953
+ flags,
954
+ "readiness-timeout-ms",
955
+ fallback.readinessTimeoutMs ?? fallback.timeoutMs,
956
+ ),
957
+ senderTps: numberFlag(flags, "sender-tps", fallback.senderTps ?? 40),
958
+ rateLimitBackoffMs: numberFlag(
959
+ flags,
960
+ "rate-limit-backoff-ms",
961
+ fallback.rateLimitBackoffMs ?? 350,
962
+ ),
963
+ jitterMs: numberFlag(flags, "retry-jitter-ms", fallback.jitterMs ?? 80),
964
+ };
965
+ }
966
+
967
+ type RawBuyPlanRow = Record<string, unknown>;
968
+
969
+ function stringValue(row: RawBuyPlanRow, key: string): string | undefined {
970
+ const value = row[key];
971
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
972
+ }
973
+
974
+ function numberValue(row: RawBuyPlanRow, key: string): number | undefined {
975
+ const value = row[key];
976
+ if (value == null || value === "") return undefined;
977
+ const parsed = Number(value);
978
+ if (!Number.isFinite(parsed))
979
+ throw new Error(`Invalid buy plan ${key}: ${String(value)}`);
980
+ return parsed;
981
+ }
982
+
983
+ function bigintValue(row: RawBuyPlanRow, key: string): bigint | undefined {
984
+ const value = row[key];
985
+ if (value == null || value === "") return undefined;
986
+ try {
987
+ return BigInt(String(value));
988
+ } catch {
989
+ throw new Error(`Invalid buy plan ${key}: ${String(value)}`);
990
+ }
991
+ }
992
+
993
+ function solValue(
994
+ row: RawBuyPlanRow,
995
+ solKey: string,
996
+ lamportsKey: string,
997
+ ): bigint | undefined {
998
+ const lamports = bigintValue(row, lamportsKey);
999
+ if (lamports != null) return lamports;
1000
+ const human = stringValue(row, solKey);
1001
+ return human == null ? undefined : sol(human).raw;
1002
+ }
1003
+
1004
+ function parseBuyPlanRows(flags: Flags): ExplicitBuyerPlanRow[] | null {
1005
+ const inline = first(flags, "buy-plan-json");
1006
+ const path = first(flags, "buy-plan");
1007
+ if (!inline && !path) return null;
1008
+
1009
+ const parsed = JSON.parse(
1010
+ inline ?? readFileSync(resolve(path!), "utf8"),
1011
+ ) as unknown;
1012
+ const rows = Array.isArray(parsed)
1013
+ ? parsed
1014
+ : (parsed as { buys?: unknown }).buys;
1015
+ if (!Array.isArray(rows))
1016
+ throw new Error(
1017
+ "Buy plan must be a JSON array or an object with a buys array",
1018
+ );
1019
+
1020
+ return rows.map((raw, index): ExplicitBuyerPlanRow => {
1021
+ if (!raw || typeof raw !== "object")
1022
+ throw new Error(`Buy plan row ${index + 1} must be an object`);
1023
+ const row = raw as RawBuyPlanRow;
1024
+ const wallet =
1025
+ stringValue(row, "wallet") ??
1026
+ stringValue(row, "walletRef") ??
1027
+ stringValue(row, "address");
1028
+ if (!wallet) throw new Error(`Buy plan row ${index + 1} is missing wallet`);
1029
+
1030
+ const amountMode =
1031
+ stringValue(row, "amountMode") ?? stringValue(row, "mode") ?? "range-bps";
1032
+ let amount: ExplicitBuyerPlanRow["amount"];
1033
+ if (amountMode === "exact-sol" || amountMode === "exact") {
1034
+ const exactSol =
1035
+ stringValue(row, "exactSol") ??
1036
+ stringValue(row, "sol") ??
1037
+ stringValue(row, "amountSol");
1038
+ if (!exactSol)
1039
+ throw new Error(
1040
+ `Buy plan row ${index + 1} exact-sol mode requires exactSol`,
1041
+ );
1042
+ amount = { kind: "exact-sol", sol: exactSol };
1043
+ } else if (amountMode === "exact-lamports" || amountMode === "lamports") {
1044
+ const lamports =
1045
+ bigintValue(row, "exactLamports") ??
1046
+ bigintValue(row, "lamports") ??
1047
+ bigintValue(row, "amountLamports");
1048
+ if (lamports == null)
1049
+ throw new Error(
1050
+ `Buy plan row ${index + 1} exact-lamports mode requires exactLamports`,
1051
+ );
1052
+ amount = { kind: "exact-lamports", lamports };
1053
+ } else {
1054
+ amount = {
1055
+ kind: "balance-bps",
1056
+ minBps:
1057
+ numberValue(row, "minBps") ?? numberValue(row, "buyerMinBps") ?? 5000,
1058
+ maxBps:
1059
+ numberValue(row, "maxBps") ?? numberValue(row, "buyerMaxBps") ?? 8000,
1060
+ reserveLamports:
1061
+ solValue(row, "reserveSol", "reserveLamports") ?? 20_000_000n,
1062
+ };
1063
+ }
1064
+
1065
+ const tipLamports = solValue(row, "tipSol", "tipLamports");
1066
+ return {
1067
+ wallet,
1068
+ amount,
1069
+ execution: {
1070
+ label: stringValue(row, "label"),
1071
+ sender: stringValue(row, "sender") as never,
1072
+ strategy: stringValue(row, "strategy") as never,
1073
+ tipLamports,
1074
+ cuLimit: numberValue(row, "cuLimit"),
1075
+ priorityMicroLamports: numberValue(row, "priorityMicroLamports"),
1076
+ slippageBps: numberValue(row, "slippageBps"),
1077
+ retryIntervalMs: numberValue(row, "retryIntervalMs"),
1078
+ recompileIntervalMs: numberValue(row, "recompileIntervalMs"),
1079
+ freshQuoteDelayMs: numberValue(row, "freshQuoteDelayMs"),
1080
+ maxFailedAttempts: numberValue(row, "maxFailedAttempts"),
1081
+ },
1082
+ };
1083
+ });
1084
+ }
1085
+
1086
+ export async function preparePumpTokenLaunchFromFlags(args: {
1087
+ slrd: Solard;
1088
+ flags: Flags;
1089
+ token: TokenMetadata;
1090
+ creator: string;
1091
+ env?: PumpLaunchEnvironment;
1092
+ options?: PumpTokenLaunchCliOptions;
1093
+ mint?: Keypair;
1094
+ cashback?: boolean;
1095
+ mayhemMode?: boolean;
1096
+ }): Promise<PumpTokenLaunchPlan> {
1097
+ const options = args.options ?? {};
1098
+ const group = first(args.flags, "buyer-group");
1099
+ const env = args.env ?? pumpLaunchEnvironmentFromFlags(args.flags);
1100
+ const explicitBuyPlan = parseBuyPlanRows(args.flags);
1101
+ let traders = explicitBuyPlan
1102
+ ? await loadExplicitBuyerAllocations({
1103
+ slrd: args.slrd,
1104
+ rows: explicitBuyPlan,
1105
+ excludeWallet: args.creator,
1106
+ })
1107
+ : group
1108
+ ? await loadGroupBuyerAllocations({
1109
+ slrd: args.slrd,
1110
+ group,
1111
+ minBps: numberFlag(args.flags, "buyer-min-bps", 10),
1112
+ maxBps: numberFlag(args.flags, "buyer-max-bps", 10),
1113
+ reserveLamports: optionalSol(
1114
+ args.flags,
1115
+ "buyer-reserve-sol",
1116
+ "buyer-reserve-lamports",
1117
+ 10_000_000n,
1118
+ ),
1119
+ excludeWallet: args.creator,
1120
+ })
1121
+ : [];
1122
+
1123
+ const effectiveSubmitMode = normalizeTraderSubmitMode(
1124
+ first(args.flags, "submit-mode") ?? env.submitMode,
1125
+ );
1126
+ const jitoTipLamports = env.policy.jitoTip.lamports ?? 0n;
1127
+ if (
1128
+ effectiveSubmitMode === "jito-bundle" &&
1129
+ traders.length > 0 &&
1130
+ jitoTipLamports > 0n
1131
+ ) {
1132
+ const finalIndex = traders.length - 1;
1133
+ const finalTrader = traders[finalIndex]!;
1134
+ const reserveLamports = finalTrader.reserveLamports + jitoTipLamports;
1135
+ const spendLamports =
1136
+ finalTrader.selectedBps == null
1137
+ ? finalTrader.spendLamports
1138
+ : ((finalTrader.balanceLamports - reserveLamports) *
1139
+ BigInt(finalTrader.selectedBps)) /
1140
+ 10_000n;
1141
+ if (
1142
+ spendLamports <= 0n ||
1143
+ finalTrader.balanceLamports <= spendLamports + reserveLamports
1144
+ ) {
1145
+ throw new Error(
1146
+ `Final Jito buyer ${finalTrader.address} cannot cover buy, reserve, and tip: balance=${finalTrader.balanceLamports} spend=${spendLamports} reserve=${finalTrader.reserveLamports} jitoTip=${jitoTipLamports}`,
1147
+ );
1148
+ }
1149
+ traders = traders.map((trader, index) =>
1150
+ index === finalIndex
1151
+ ? { ...trader, reserveLamports, spendLamports }
1152
+ : trader,
1153
+ );
1154
+ }
1155
+
1156
+ return await preparePumpTokenLaunch({
1157
+ slrd: args.slrd,
1158
+ token: args.token,
1159
+ creatorWallet: args.creator,
1160
+ traders,
1161
+ creatorBuyLamports: optionalSol(
1162
+ args.flags,
1163
+ "creator-buy-sol",
1164
+ "creator-buy-lamports",
1165
+ 0n,
1166
+ ),
1167
+ creatorReserveLamports: optionalSol(
1168
+ args.flags,
1169
+ "creator-reserve-sol",
1170
+ "creator-reserve-lamports",
1171
+ 10_000_000n,
1172
+ ),
1173
+ slippageBps: numberFlag(
1174
+ args.flags,
1175
+ "slippage-bps",
1176
+ options.defaultSlippageBps ?? 1500,
1177
+ ),
1178
+ cuLimit: env.cuLimit,
1179
+ priorityMicroLamports: numberFlag(
1180
+ args.flags,
1181
+ "deployment-priority-micro-lamports",
1182
+ options.defaultDeploymentPriorityMicroLamports ??
1183
+ env.priorityMicroLamports,
1184
+ ),
1185
+ buyerPriorityMicroLamports: numberFlag(
1186
+ args.flags,
1187
+ "buyer-priority-micro-lamports",
1188
+ options.defaultBuyerPriorityMicroLamports ?? 1_500_000,
1189
+ ),
1190
+ senderPolicy: env.policy,
1191
+ mint: args.mint,
1192
+ cashback: args.cashback ?? args.token.cashback ?? false,
1193
+ mayhemMode: args.mayhemMode ?? args.token.mayhemMode ?? false,
1194
+ });
1195
+ }
1196
+
1197
+ function vanitySuffixFromFlags(flags: Flags): string | null {
1198
+ const explicit = first(flags, "mint-suffix") ?? first(flags, "vanity-suffix");
1199
+ if (explicit && explicit !== "true") return explicit.trim();
1200
+ if (enabled(flags, "pump-suffix", "SOLARD_LAUNCH_PUMP_SUFFIX")) return "pump";
1201
+ return null;
1202
+ }
1203
+
1204
+ function envNumber(name: string, fallback: number): number {
1205
+ const raw = process.env[name]?.trim();
1206
+ if (!raw) return fallback;
1207
+ const parsed = Number(raw);
1208
+ return Number.isFinite(parsed) ? parsed : fallback;
1209
+ }
1210
+
1211
+ export async function runPumpTokenLaunchFromArgs(
1212
+ argv: string[],
1213
+ options: PumpTokenLaunchCliOptions = {},
1214
+ ): Promise<PumpTokenLaunchCliResult> {
1215
+ const { flags } = parseArgs(argv);
1216
+ const report = options.report ?? defaultReport;
1217
+ const creator = required(flags, "creator");
1218
+ const input = pumpTokenMetadataInput(flags);
1219
+ const live = enabled(flags, "live", "LIVE");
1220
+ const skipSimulation = enabled(flags, "skip-simulation", "SKIP_SIMULATION");
1221
+ const group = first(flags, "buyer-group");
1222
+ const hasExplicitBuyPlan = Boolean(
1223
+ first(flags, "buy-plan") || first(flags, "buy-plan-json"),
1224
+ );
1225
+ const hasBuyers = Boolean(group) || hasExplicitBuyPlan;
1226
+ const deployOnly = enabled(flags, "deploy-only");
1227
+ if (deployOnly && hasBuyers) {
1228
+ throw new Error(
1229
+ "--deploy-only cannot be combined with --buyer-group, --buy-plan, or --buy-plan-json. Armed buyers must own the follower plan.",
1230
+ );
1231
+ }
1232
+ const configuredEnv = pumpLaunchEnvironmentFromFlags(flags);
1233
+ const submitMode = resolveSubmitMode(flags, options);
1234
+ let env: PumpLaunchEnvironment =
1235
+ submitMode === "jito-bundle"
1236
+ ? {
1237
+ ...configuredEnv,
1238
+ policy: {
1239
+ ...configuredEnv.policy,
1240
+ deploymentSender: "jito",
1241
+ evolutionSender: "jito",
1242
+ fastTraderSender: "jito",
1243
+ rpcTraderSender: "jito",
1244
+ fastTraderCount: Number.MAX_SAFE_INTEGER,
1245
+ },
1246
+ }
1247
+ : configuredEnv;
1248
+ const spam = spamOptionsFromFlags(flags, env.spam);
1249
+ const usesHeliusSender = usesHeliusSenderForLaunch(env, hasBuyers);
1250
+ const persistOnLive = options.persistOnLive ?? true;
1251
+
1252
+ if (usesHeliusSender && !env.senderUrl) {
1253
+ throw new Error(
1254
+ "Helius fast sender is selected; provide --helius-sender-url or HELIUS_SENDER_URL.",
1255
+ );
1256
+ }
1257
+
1258
+ if (usesHeliusSender) {
1259
+ validateHeliusTip({
1260
+ tip: env.policy.fastTip,
1261
+ endpoint: env.senderUrl ?? "",
1262
+ live,
1263
+ label: "Pump launch transactions routed through Helius Sender",
1264
+ });
1265
+ }
1266
+
1267
+ if (submitMode === "jito-bundle") {
1268
+ if (!hasBuyers) {
1269
+ throw new Error(
1270
+ "jito-bundle requires at least one buyer in --buy-plan, --buy-plan-json, or --buyer-group.",
1271
+ );
1272
+ }
1273
+ if (!env.jitoUrl) {
1274
+ throw new Error(
1275
+ "jito-bundle requires --jito-block-engine-url or JITO_BLOCK_ENGINE_URL.",
1276
+ );
1277
+ }
1278
+ }
1279
+
1280
+ requireSkipSimulationForBlindLiveBuys(
1281
+ live,
1282
+ submitMode,
1283
+ skipSimulation,
1284
+ hasBuyers,
1285
+ );
1286
+
1287
+ const { uri, uploaded } = await resolveMetadataUri(flags, input);
1288
+ const cashback = enabled(flags, "cashback", "SOLARD_LAUNCH_CASHBACK");
1289
+ const mayhemMode = enabled(flags, "mayhem", "SOLARD_LAUNCH_MAYHEM");
1290
+ const vanityMintSuffix = vanitySuffixFromFlags(flags);
1291
+ const pregeneratedMint = loadMintKeypairFromFlags(flags, vanityMintSuffix);
1292
+ let vanityMint: Keypair | undefined = pregeneratedMint?.mint;
1293
+ let vanityMintAttempts: number | null = pregeneratedMint ? 0 : null;
1294
+ let vanityMintElapsedMs: number | null = pregeneratedMint ? 0 : null;
1295
+
1296
+ if (pregeneratedMint) {
1297
+ report("pregenerated mint loaded", {
1298
+ mint: pregeneratedMint.address,
1299
+ keypairPath: pregeneratedMint.path,
1300
+ suffix: vanityMintSuffix,
1301
+ attempts: 0,
1302
+ elapsedMs: 0,
1303
+ });
1304
+ } else if (vanityMintSuffix) {
1305
+ const maxAttempts = numberFlag(
1306
+ flags,
1307
+ "vanity-max-attempts",
1308
+ envNumber("SOLARD_VANITY_MINT_MAX_ATTEMPTS", 25_000_000),
1309
+ );
1310
+ const timeoutMs = numberFlag(
1311
+ flags,
1312
+ "vanity-timeout-ms",
1313
+ envNumber("SOLARD_VANITY_MINT_TIMEOUT_MS", 0),
1314
+ );
1315
+ const reportEvery = numberFlag(
1316
+ flags,
1317
+ "vanity-report-every",
1318
+ envNumber("SOLARD_VANITY_MINT_REPORT_EVERY", 10_000),
1319
+ );
1320
+ report("vanity mint start", {
1321
+ suffix: vanityMintSuffix,
1322
+ maxAttempts,
1323
+ timeoutMs,
1324
+ reportEvery,
1325
+ });
1326
+ const found = await generateMintKeypairWithSuffix({
1327
+ suffix: vanityMintSuffix,
1328
+ maxAttempts,
1329
+ timeoutMs,
1330
+ reportEvery,
1331
+ onProgress: (progress) => report("vanity mint progress", progress),
1332
+ });
1333
+ vanityMint = found.mint;
1334
+ vanityMintAttempts = found.attempts;
1335
+ vanityMintElapsedMs = found.elapsedMs;
1336
+ report("vanity mint found", {
1337
+ mint: found.mint.publicKey.toBase58(),
1338
+ suffix: vanityMintSuffix,
1339
+ attempts: found.attempts,
1340
+ elapsedMs: found.elapsedMs,
1341
+ });
1342
+ }
1343
+
1344
+ const armedEndpointValues = flags.get("armed-buyer") ?? [];
1345
+ const armedEndpoints = armedEndpointValues.map(parseArmedBuyerEndpoint);
1346
+ const armedSession = first(flags, "session")?.trim() ?? "";
1347
+ if (armedEndpoints.length > 1) {
1348
+ throw new Error(
1349
+ "v57 accepts one --armed-buyer endpoint containing the complete buy plan, so worst-case quotes include every buyer.",
1350
+ );
1351
+ }
1352
+ if (armedEndpoints.length > 0 && !armedSession) {
1353
+ throw new Error("--session is required when --armed-buyer is used.");
1354
+ }
1355
+ if (armedEndpoints.length > 0 && !deployOnly) {
1356
+ throw new Error("--armed-buyer requires --deploy-only.");
1357
+ }
1358
+ if (armedEndpoints.length > 0 && !live) {
1359
+ throw new Error("--armed-buyer release is live-only.");
1360
+ }
1361
+
1362
+ let jitoTipSelection: JitoTipSelection | null = null;
1363
+ if (submitMode === "jito-bundle") {
1364
+ jitoTipSelection = await resolveJitoBundleTip({
1365
+ flags,
1366
+ configuredLamports: env.policy.jitoTip.lamports ?? 100_000n,
1367
+ });
1368
+ env = {
1369
+ ...env,
1370
+ policy: {
1371
+ ...env.policy,
1372
+ jitoTip: {
1373
+ ...env.policy.jitoTip,
1374
+ lamports: jitoTipSelection.lamports,
1375
+ },
1376
+ },
1377
+ };
1378
+ validateJitoTip({
1379
+ tip: env.policy.jitoTip,
1380
+ live,
1381
+ label: "Pump Jito bundle",
1382
+ });
1383
+ if (jitoTipSelection.source === "minimum-fallback") {
1384
+ report("jito tip floor fallback", {
1385
+ endpoint: jitoTipSelection.endpoint,
1386
+ reason: jitoTipSelection.fallbackReason,
1387
+ startingLamports: jitoTipSelection.lamports.toString(),
1388
+ selectedSol: jitoTipSelection.selectedSol,
1389
+ policy: "start-at-minimum-and-escalate-fresh-generations",
1390
+ });
1391
+ }
1392
+
1393
+ report("jito tip selected", {
1394
+ ...jitoTipSelection,
1395
+ lamports: jitoTipSelection.lamports.toString(),
1396
+ });
1397
+ }
1398
+
1399
+ const slrd = createTraderSolard({ rpcUrl: env.rpcUrl });
1400
+ installPumpLaunchSenders(slrd, env);
1401
+
1402
+ if (vanityMint) {
1403
+ await assertPumpMintIsUnused({
1404
+ slrd,
1405
+ mint: vanityMint.publicKey,
1406
+ report,
1407
+ });
1408
+ }
1409
+
1410
+ let prepared: PumpTokenLaunchPlan | null = null;
1411
+ try {
1412
+ const token: TokenMetadata = {
1413
+ alias: input.alias,
1414
+ name: input.name,
1415
+ symbol: input.symbol,
1416
+ uri,
1417
+ cashback,
1418
+ mayhemMode,
1419
+ };
1420
+ const deploymentPriorityMicroLamports = numberFlag(
1421
+ flags,
1422
+ "deployment-priority-micro-lamports",
1423
+ options.defaultDeploymentPriorityMicroLamports ??
1424
+ env.priorityMicroLamports,
1425
+ );
1426
+ const buyerPriorityMicroLamports = numberFlag(
1427
+ flags,
1428
+ "buyer-priority-micro-lamports",
1429
+ options.defaultBuyerPriorityMicroLamports ?? 1_500_000,
1430
+ );
1431
+ const slippageBps = numberFlag(
1432
+ flags,
1433
+ "slippage-bps",
1434
+ options.defaultSlippageBps ?? 1500,
1435
+ );
1436
+
1437
+ prepared = await preparePumpTokenLaunchFromFlags({
1438
+ slrd,
1439
+ flags,
1440
+ token,
1441
+ creator,
1442
+ env,
1443
+ options,
1444
+ mint: vanityMint,
1445
+ cashback,
1446
+ mayhemMode,
1447
+ });
1448
+ const mint = prepared.deployment.mint.publicKey.toBase58();
1449
+
1450
+ report("pump launch plan", {
1451
+ live,
1452
+ token: {
1453
+ alias: input.alias,
1454
+ name: input.name,
1455
+ symbol: input.symbol,
1456
+ mint,
1457
+ metadataUri: uri,
1458
+ website: input.website ?? null,
1459
+ twitter: input.twitter ?? null,
1460
+ telegram: input.telegram ?? null,
1461
+ video: input.video ?? null,
1462
+ showName: input.showName ?? true,
1463
+ },
1464
+ metadata: uploaded
1465
+ ? {
1466
+ provider: uploaded.provider,
1467
+ metadataUri: uploaded.metadataUri,
1468
+ imageUri: uploaded.imageUri ?? null,
1469
+ website: input.website ?? null,
1470
+ twitter: input.twitter ?? null,
1471
+ telegram: input.telegram ?? null,
1472
+ video: input.video ?? null,
1473
+ showName: input.showName ?? true,
1474
+ }
1475
+ : null,
1476
+ feeMode: "creator-fees",
1477
+ cashback,
1478
+ mayhemMode,
1479
+ vanityMintSuffix,
1480
+ vanityMintAttempts,
1481
+ vanityMintElapsedMs,
1482
+ creatorBuyLamports: optionalSol(
1483
+ flags,
1484
+ "creator-buy-sol",
1485
+ "creator-buy-lamports",
1486
+ 0n,
1487
+ ),
1488
+ buyerGroup: group ?? null,
1489
+ buyPlan:
1490
+ first(flags, "buy-plan") ??
1491
+ (first(flags, "buy-plan-json") ? "inline-json" : null),
1492
+ transport: {
1493
+ usesHeliusSender,
1494
+ priorityMicroLamports: deploymentPriorityMicroLamports,
1495
+ buyerPriorityMicroLamports,
1496
+ heliusTipLamports: env.policy.fastTip.lamports ?? null,
1497
+ jitoUrl: submitMode === "jito-bundle" ? env.jitoUrl : null,
1498
+ jitoTipLamports:
1499
+ submitMode === "jito-bundle"
1500
+ ? (env.policy.jitoTip.lamports ?? null)
1501
+ : null,
1502
+ jitoTipSelection,
1503
+ senderTps: spam.senderTps ?? null,
1504
+ },
1505
+ routes: {
1506
+ createAndCreatorBuy: env.policy.deploymentSender,
1507
+ fastBuyerCount: env.policy.fastTraderCount,
1508
+ fastBuyers: env.policy.fastTraderSender,
1509
+ remainingBuyers: env.policy.rpcTraderSender,
1510
+ },
1511
+ execution: {
1512
+ submitMode,
1513
+ skipSimulation,
1514
+ slippageBps,
1515
+ retry: spam,
1516
+ },
1517
+ participants: prepared.expectedOutputByWallet,
1518
+ });
1519
+
1520
+ const armedMint = prepared.deployment.mint.publicKey.toBase58();
1521
+ const armedTimeoutMs = numberFlag(flags, "armed-timeout-ms", 1_000);
1522
+
1523
+ if (armedEndpoints.length > 0) {
1524
+ await assertArmedBuyerEndpointsReady({
1525
+ endpoints: armedEndpoints,
1526
+ session: armedSession,
1527
+ mint: armedMint,
1528
+ timeoutMs: armedTimeoutMs,
1529
+ });
1530
+ report("pump armed buyers ready", {
1531
+ session: armedSession,
1532
+ mint: armedMint,
1533
+ endpoints: armedEndpoints.map((endpoint) => ({
1534
+ label: endpoint.label,
1535
+ address: `${endpoint.host}:${endpoint.port}`,
1536
+ })),
1537
+ });
1538
+ }
1539
+
1540
+ const result = await executePumpTokenLaunch({
1541
+ slrd,
1542
+ prepared,
1543
+ live,
1544
+ traderSubmitMode: submitMode,
1545
+ skipSimulation,
1546
+ spam,
1547
+ kind: `cli:launch:pump:${input.alias}`,
1548
+ reporter: report,
1549
+ beforeDeploymentBroadcast:
1550
+ armedEndpoints.length > 0
1551
+ ? async () => {
1552
+ await releaseArmedBuyerEndpoints({
1553
+ endpoints: armedEndpoints,
1554
+ session: armedSession,
1555
+ mint: armedMint,
1556
+ timeoutMs: armedTimeoutMs,
1557
+ reporter: report,
1558
+ });
1559
+ }
1560
+ : undefined,
1561
+ onDeploymentBroadcastFailure:
1562
+ armedEndpoints.length > 0
1563
+ ? async (error) => {
1564
+ await abortArmedBuyerEndpoints({
1565
+ endpoints: armedEndpoints,
1566
+ session: armedSession,
1567
+ mint: armedMint,
1568
+ reason: error instanceof Error ? error.message : String(error),
1569
+ reporter: report,
1570
+ });
1571
+ }
1572
+ : undefined,
1573
+ });
1574
+
1575
+ if (live && persistOnLive && prepared) {
1576
+ try {
1577
+ slrd.persistPreparedDeployment(prepared.deployment, input.alias);
1578
+ } catch {
1579
+ // The launch already succeeded. Persistence is best-effort and must not
1580
+ // alter the on-chain result.
1581
+ }
1582
+ }
1583
+
1584
+ const output: PumpTokenLaunchCliResult = {
1585
+ createdAt: new Date().toISOString(),
1586
+ live,
1587
+ creator,
1588
+ buyerGroup: group ?? null,
1589
+ buyPlan:
1590
+ first(flags, "buy-plan") ??
1591
+ (first(flags, "buy-plan-json") ? "inline-json" : null),
1592
+ transport: {
1593
+ usesHeliusSender,
1594
+ priorityMicroLamports: deploymentPriorityMicroLamports,
1595
+ buyerPriorityMicroLamports,
1596
+ heliusTipLamports: env.policy.fastTip.lamports ?? null,
1597
+ jitoUrl: submitMode === "jito-bundle" ? env.jitoUrl : null,
1598
+ jitoTipLamports:
1599
+ submitMode === "jito-bundle"
1600
+ ? (env.policy.jitoTip.lamports ?? null)
1601
+ : null,
1602
+ jitoTipSelection,
1603
+ senderTps: spam.senderTps ?? null,
1604
+ createAndCreatorBuy: env.policy.deploymentSender,
1605
+ fastBuyerCount: env.policy.fastTraderCount,
1606
+ fastBuyers: env.policy.fastTraderSender,
1607
+ remainingBuyers: env.policy.rpcTraderSender,
1608
+ submitMode,
1609
+ },
1610
+ token: {
1611
+ ...input,
1612
+ uri,
1613
+ metadataUri: uri,
1614
+ mint,
1615
+ feeMode: "creator-fees",
1616
+ cashback,
1617
+ mayhemMode,
1618
+ vanityMintSuffix,
1619
+ vanityMintAttempts,
1620
+ vanityMintElapsedMs,
1621
+ },
1622
+ result,
1623
+ };
1624
+
1625
+ const out = first(flags, "out");
1626
+ if (out) {
1627
+ const path = resolve(out);
1628
+ mkdirSync(dirname(path), { recursive: true });
1629
+ writeFileSync(path, json(output));
1630
+ }
1631
+
1632
+ report("pump launch result", {
1633
+ alias: input.alias,
1634
+ mint,
1635
+ live,
1636
+ outputPath: out ? resolve(out) : null,
1637
+ result: summarizeLaunchResult(result),
1638
+ });
1639
+
1640
+ return output;
1641
+ } finally {
1642
+ // Never persist from finally: preflight, simulation, and bundle submission
1643
+ // failures must leave the token registry untouched.
1644
+ slrd.close();
1645
+ }
1646
+ }
1647
+
1648
+ export function formatCliError(error: unknown): string {
1649
+ return error instanceof Error
1650
+ ? (error.stack ?? error.message)
1651
+ : String(error);
1652
+ }