@solard/cli 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,8 @@ import {
7
7
  assertArmedBuyerEndpointsReady,
8
8
  bondingCurvePda,
9
9
  createTraderSolard,
10
+ cleanVanitySuffix,
11
+ defaultVanityMaxAttempts,
10
12
  executePumpTokenLaunch,
11
13
  generateMintKeypairWithSuffix,
12
14
  installPumpLaunchSenders,
@@ -18,6 +20,9 @@ import {
18
20
  preparePumpTokenLaunch,
19
21
  pumpLaunchEnvironment,
20
22
  releaseArmedBuyerEndpoints,
23
+ releaseVanityMintReservation,
24
+ reserveVanityMintFromPool,
25
+ markVanityMintUsed,
21
26
  sol,
22
27
  TOKEN_2022_ID,
23
28
  uploadPumpMetadata,
@@ -35,6 +40,7 @@ import {
35
40
  type Solard,
36
41
  type TokenMetadata,
37
42
  type TraderSubmitMode,
43
+ type VanityMintPoolReservation,
38
44
  } from "@solard/sdk";
39
45
 
40
46
  export type Flags = Map<string, string[]>;
@@ -1027,30 +1033,61 @@ function parseBuyPlanRows(flags: Flags): ExplicitBuyerPlanRow[] | null {
1027
1033
  stringValue(row, "address");
1028
1034
  if (!wallet) throw new Error(`Buy plan row ${index + 1} is missing wallet`);
1029
1035
 
1036
+ const exactSol =
1037
+ stringValue(row, "exactSol") ??
1038
+ stringValue(row, "sol") ??
1039
+ stringValue(row, "amountSol");
1040
+ const exactLamports =
1041
+ bigintValue(row, "exactLamports") ??
1042
+ bigintValue(row, "lamports") ??
1043
+ bigintValue(row, "amountLamports");
1044
+
1045
+ if (exactSol != null && exactLamports != null) {
1046
+ throw new Error(
1047
+ `Buy plan row ${index + 1} supplies both SOL and lamport exact amounts`,
1048
+ );
1049
+ }
1050
+
1051
+ const configuredMode =
1052
+ stringValue(row, "amountMode") ?? stringValue(row, "mode");
1030
1053
  const amountMode =
1031
- stringValue(row, "amountMode") ?? stringValue(row, "mode") ?? "range-bps";
1054
+ configuredMode ??
1055
+ (exactSol != null
1056
+ ? "exact-sol"
1057
+ : exactLamports != null
1058
+ ? "exact-lamports"
1059
+ : "range-bps");
1060
+
1061
+ const exactReserveLamports =
1062
+ solValue(row, "reserveSol", "reserveLamports") ?? 0n;
1063
+
1032
1064
  let amount: ExplicitBuyerPlanRow["amount"];
1033
1065
  if (amountMode === "exact-sol" || amountMode === "exact") {
1034
- const exactSol =
1035
- stringValue(row, "exactSol") ??
1036
- stringValue(row, "sol") ??
1037
- stringValue(row, "amountSol");
1038
1066
  if (!exactSol)
1039
1067
  throw new Error(
1040
- `Buy plan row ${index + 1} exact-sol mode requires exactSol`,
1068
+ `Buy plan row ${index + 1} exact-sol mode requires exactSol/amountSol`,
1041
1069
  );
1042
- amount = { kind: "exact-sol", sol: exactSol };
1070
+ amount = {
1071
+ kind: "exact-sol",
1072
+ sol: exactSol,
1073
+ reserveLamports: exactReserveLamports,
1074
+ };
1043
1075
  } 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)
1076
+ if (exactLamports == null)
1049
1077
  throw new Error(
1050
- `Buy plan row ${index + 1} exact-lamports mode requires exactLamports`,
1078
+ `Buy plan row ${index + 1} exact-lamports mode requires exactLamports/amountLamports`,
1051
1079
  );
1052
- amount = { kind: "exact-lamports", lamports };
1080
+ amount = {
1081
+ kind: "exact-lamports",
1082
+ lamports: exactLamports,
1083
+ reserveLamports: exactReserveLamports,
1084
+ };
1053
1085
  } else {
1086
+ if (exactSol != null || exactLamports != null) {
1087
+ throw new Error(
1088
+ `Buy plan row ${index + 1} exact amount conflicts with amountMode ${amountMode}`,
1089
+ );
1090
+ }
1054
1091
  amount = {
1055
1092
  kind: "balance-bps",
1056
1093
  minBps:
@@ -1140,7 +1177,7 @@ export async function preparePumpTokenLaunchFromFlags(args: {
1140
1177
  10_000n;
1141
1178
  if (
1142
1179
  spendLamports <= 0n ||
1143
- finalTrader.balanceLamports <= spendLamports + reserveLamports
1180
+ finalTrader.balanceLamports < spendLamports + reserveLamports
1144
1181
  ) {
1145
1182
  throw new Error(
1146
1183
  `Final Jito buyer ${finalTrader.address} cannot cover buy, reserve, and tip: balance=${finalTrader.balanceLamports} spend=${spendLamports} reserve=${finalTrader.reserveLamports} jitoTip=${jitoTipLamports}`,
@@ -1287,8 +1324,32 @@ export async function runPumpTokenLaunchFromArgs(
1287
1324
  const { uri, uploaded } = await resolveMetadataUri(flags, input);
1288
1325
  const cashback = enabled(flags, "cashback", "SOLARD_LAUNCH_CASHBACK");
1289
1326
  const mayhemMode = enabled(flags, "mayhem", "SOLARD_LAUNCH_MAYHEM");
1290
- const vanityMintSuffix = vanitySuffixFromFlags(flags);
1327
+ const vanityPoolSuffixRaw = nonEmpty(first(flags, "mint-pool"));
1328
+ const vanityPoolAddress = nonEmpty(first(flags, "mint-pool-address"));
1329
+ if (vanityPoolAddress && !vanityPoolSuffixRaw) {
1330
+ throw new Error("--mint-pool-address requires --mint-pool <suffix>.");
1331
+ }
1332
+ if (vanityPoolSuffixRaw && first(flags, "mint-keypair")) {
1333
+ throw new Error("Use either --mint-pool or --mint-keypair, not both.");
1334
+ }
1335
+
1336
+ const vanityPoolSuffix = vanityPoolSuffixRaw
1337
+ ? cleanVanitySuffix(vanityPoolSuffixRaw)
1338
+ : null;
1339
+ const configuredVanitySuffix = vanitySuffixFromFlags(flags);
1340
+ if (
1341
+ vanityPoolSuffix &&
1342
+ configuredVanitySuffix &&
1343
+ vanityPoolSuffix !== configuredVanitySuffix
1344
+ ) {
1345
+ throw new Error(
1346
+ `--mint-pool suffix ${vanityPoolSuffix} conflicts with configured mint suffix ${configuredVanitySuffix}.`,
1347
+ );
1348
+ }
1349
+
1350
+ const vanityMintSuffix = vanityPoolSuffix ?? configuredVanitySuffix;
1291
1351
  const pregeneratedMint = loadMintKeypairFromFlags(flags, vanityMintSuffix);
1352
+ let poolReservation: VanityMintPoolReservation | null = null;
1292
1353
  let vanityMint: Keypair | undefined = pregeneratedMint?.mint;
1293
1354
  let vanityMintAttempts: number | null = pregeneratedMint ? 0 : null;
1294
1355
  let vanityMintElapsedMs: number | null = pregeneratedMint ? 0 : null;
@@ -1301,11 +1362,14 @@ export async function runPumpTokenLaunchFromArgs(
1301
1362
  attempts: 0,
1302
1363
  elapsedMs: 0,
1303
1364
  });
1304
- } else if (vanityMintSuffix) {
1365
+ } else if (vanityMintSuffix && !vanityPoolSuffix) {
1305
1366
  const maxAttempts = numberFlag(
1306
1367
  flags,
1307
1368
  "vanity-max-attempts",
1308
- envNumber("SOLARD_VANITY_MINT_MAX_ATTEMPTS", 25_000_000),
1369
+ envNumber(
1370
+ "SOLARD_VANITY_MINT_MAX_ATTEMPTS",
1371
+ defaultVanityMaxAttempts(vanityMintSuffix),
1372
+ ),
1309
1373
  );
1310
1374
  const timeoutMs = numberFlag(
1311
1375
  flags,
@@ -1396,6 +1460,22 @@ export async function runPumpTokenLaunchFromArgs(
1396
1460
  });
1397
1461
  }
1398
1462
 
1463
+ if (!vanityMint && vanityPoolSuffix) {
1464
+ poolReservation = reserveVanityMintFromPool(vanityPoolSuffix, {
1465
+ address: vanityPoolAddress,
1466
+ reason: live ? "pump-launch-live" : "pump-launch-dry-run",
1467
+ });
1468
+ vanityMint = poolReservation.mint;
1469
+ vanityMintAttempts = 0;
1470
+ vanityMintElapsedMs = 0;
1471
+ report("vanity mint pool reserved", {
1472
+ mint: poolReservation.address,
1473
+ suffix: poolReservation.suffix,
1474
+ status: poolReservation.status,
1475
+ live,
1476
+ });
1477
+ }
1478
+
1399
1479
  const slrd = createTraderSolard({ rpcUrl: env.rpcUrl });
1400
1480
  installPumpLaunchSenders(slrd, env);
1401
1481
 
@@ -1572,6 +1652,15 @@ export async function runPumpTokenLaunchFromArgs(
1572
1652
  : undefined,
1573
1653
  });
1574
1654
 
1655
+ if (poolReservation && live) {
1656
+ const used = markVanityMintUsed(poolReservation.address);
1657
+ report("vanity mint pool used", {
1658
+ mint: used.address,
1659
+ suffix: used.suffix,
1660
+ status: used.status,
1661
+ });
1662
+ }
1663
+
1575
1664
  if (live && persistOnLive && prepared) {
1576
1665
  try {
1577
1666
  slrd.persistPreparedDeployment(prepared.deployment, input.alias);
@@ -1639,6 +1728,17 @@ export async function runPumpTokenLaunchFromArgs(
1639
1728
 
1640
1729
  return output;
1641
1730
  } finally {
1731
+ // Dry runs never consume a pooled mint. Live failures intentionally leave
1732
+ // it RESERVED because a bundle can be ambiguous even when local tracking
1733
+ // fails. Release it manually only after on-chain preflight confirms unused.
1734
+ if (poolReservation && !live) {
1735
+ try {
1736
+ releaseVanityMintReservation(poolReservation.address);
1737
+ } catch {
1738
+ // Preserve the primary launch/simulation result.
1739
+ }
1740
+ }
1741
+
1642
1742
  // Never persist from finally: preflight, simulation, and bundle submission
1643
1743
  // failures must leave the token registry untouched.
1644
1744
  slrd.close();
@@ -0,0 +1,363 @@
1
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { extname, join, resolve } from "node:path";
4
+
5
+ import {
6
+ createTraderSolard,
7
+ fetchPumpVampSourceMetadata,
8
+ publicPumpMetadataUrl,
9
+ uploadPumpMetadata,
10
+ type MetadataUploaderId,
11
+ type PumpVampSourceMetadata,
12
+ } from "@solard/sdk";
13
+
14
+ import {
15
+ first,
16
+ parseArgs,
17
+ runPumpTokenLaunchFromArgs,
18
+ type PumpTokenLaunchCliOptions,
19
+ type PumpTokenLaunchCliResult,
20
+ } from "./token-launch-cli.ts";
21
+
22
+ export type PumpVampCliResult = {
23
+ source: PumpVampSourceMetadata;
24
+ resolved: {
25
+ alias: string;
26
+ name: string;
27
+ symbol: string;
28
+ uri: string;
29
+ metadataMode: "source-uri" | "override-uri" | "merged-upload";
30
+ overrides: string[];
31
+ };
32
+ launch: PumpTokenLaunchCliResult;
33
+ };
34
+
35
+ function clean(value: string | undefined): string | undefined {
36
+ const text = value?.trim();
37
+ return text && text !== "true" ? text : undefined;
38
+ }
39
+
40
+ function safeAlias(symbol: string, sourceMint: string): string {
41
+ const stem =
42
+ symbol
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9_-]+/g, "-")
45
+ .replace(/^-+|-+$/g, "") || "token";
46
+ return `vamp-${stem}-${sourceMint.slice(0, 6).toLowerCase()}-${Date.now()
47
+ .toString(36)
48
+ .slice(-5)}`;
49
+ }
50
+
51
+ function isRemote(value: string): boolean {
52
+ return /^(?:https?:\/\/|ipfs:\/\/|ar:\/\/)/i.test(value.trim());
53
+ }
54
+
55
+ function imageExtension(value: string): string {
56
+ try {
57
+ const pathname = new URL(publicPumpMetadataUrl(value)).pathname;
58
+ const extension = extname(pathname);
59
+ return extension && extension.length <= 8 ? extension : ".img";
60
+ } catch {
61
+ const extension = extname(value);
62
+ return extension && extension.length <= 8 ? extension : ".img";
63
+ }
64
+ }
65
+
66
+ async function downloadToTemp(
67
+ value: string,
68
+ ): Promise<{ path: string; cleanup: () => void }> {
69
+ const directory = mkdtempSync(join(tmpdir(), "solard-vamp-"));
70
+ const url = publicPumpMetadataUrl(value);
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), 20_000);
73
+ try {
74
+ const response = await fetch(url, { signal: controller.signal });
75
+ if (!response.ok) {
76
+ throw new Error(
77
+ `Could not download vamp image (${response.status}) from ${url}`,
78
+ );
79
+ }
80
+ const bytes = Buffer.from(await response.arrayBuffer());
81
+ if (!bytes.length)
82
+ throw new Error(`Downloaded vamp image is empty: ${url}`);
83
+ const path = join(directory, `image${imageExtension(value)}`);
84
+ writeFileSync(path, bytes);
85
+ return {
86
+ path,
87
+ cleanup: () => rmSync(directory, { recursive: true, force: true }),
88
+ };
89
+ } catch (error) {
90
+ rmSync(directory, { recursive: true, force: true });
91
+ throw error;
92
+ } finally {
93
+ clearTimeout(timeout);
94
+ }
95
+ }
96
+
97
+ async function resolveImagePath(
98
+ configured: string | undefined,
99
+ sourceImage: string | null,
100
+ ): Promise<{ path: string; cleanup: () => void }> {
101
+ const selected = configured ?? sourceImage ?? undefined;
102
+ if (!selected) {
103
+ throw new Error(
104
+ "Vamp metadata override requires an image. Source metadata has no image; provide --image <path-or-url>.",
105
+ );
106
+ }
107
+
108
+ if (isRemote(selected)) return await downloadToTemp(selected);
109
+
110
+ const path = resolve(selected);
111
+ if (!existsSync(path)) {
112
+ throw new Error(`Vamp image file not found: ${path}`);
113
+ }
114
+ return { path, cleanup: () => {} };
115
+ }
116
+
117
+ function stripVampArgs(argv: string[]): string[] {
118
+ const handled = new Set([
119
+ "source-authority", // legacy v1 flag: intentionally ignored now
120
+ "alias",
121
+ "name",
122
+ "symbol",
123
+ "uri",
124
+ "metadata",
125
+ "image",
126
+ "description",
127
+ "website",
128
+ "twitter",
129
+ "telegram",
130
+ "video",
131
+ "show-name",
132
+ "hide-name",
133
+ "cashback",
134
+ ]);
135
+
136
+ const result: string[] = [];
137
+ let sourceMintRemoved = false;
138
+
139
+ for (let index = 0; index < argv.length; index += 1) {
140
+ const item = argv[index]!;
141
+ if (!item.startsWith("--")) {
142
+ if (!sourceMintRemoved) {
143
+ sourceMintRemoved = true;
144
+ continue;
145
+ }
146
+ result.push(item);
147
+ continue;
148
+ }
149
+
150
+ const [key, inline] = item.slice(2).split("=", 2);
151
+ if (!handled.has(key!)) {
152
+ result.push(item);
153
+ if (
154
+ inline == null &&
155
+ argv[index + 1] &&
156
+ !argv[index + 1]!.startsWith("--")
157
+ ) {
158
+ result.push(argv[++index]!);
159
+ }
160
+ continue;
161
+ }
162
+
163
+ if (
164
+ inline == null &&
165
+ argv[index + 1] &&
166
+ !argv[index + 1]!.startsWith("--")
167
+ ) {
168
+ index += 1;
169
+ }
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ function metadataProvider(
176
+ flags: ReturnType<typeof parseArgs>["flags"],
177
+ ): MetadataUploaderId {
178
+ return (clean(first(flags, "metadata-provider")) ??
179
+ clean(process.env.SLRD_METADATA_UPLOADER) ??
180
+ clean(process.env.PUMP_METADATA_PROVIDER) ??
181
+ "pump-frontend") as MetadataUploaderId;
182
+ }
183
+
184
+ export async function runPumpVampFromArgs(
185
+ argv: string[],
186
+ options: PumpTokenLaunchCliOptions = {},
187
+ ): Promise<PumpVampCliResult> {
188
+ const { flags, positionals } = parseArgs(argv);
189
+ if (positionals.length !== 1) {
190
+ throw new Error(
191
+ "Usage: slrd vamp <source-mint> --creator <wallet> [metadata overrides] [launch flags]",
192
+ );
193
+ }
194
+ if (first(flags, "metadata")) {
195
+ throw new Error(
196
+ "vamp does not accept --metadata <file>; override individual fields or provide --uri <metadata-uri>.",
197
+ );
198
+ }
199
+
200
+ const sourceMint = positionals[0]!;
201
+ const report = options.report ?? (() => {});
202
+ const sourceSlrd = createTraderSolard();
203
+ let source: PumpVampSourceMetadata;
204
+ try {
205
+ source = await fetchPumpVampSourceMetadata(
206
+ sourceSlrd.connection(),
207
+ sourceMint,
208
+ );
209
+ } finally {
210
+ sourceSlrd.close();
211
+ }
212
+
213
+ const alias =
214
+ clean(first(flags, "alias")) ?? safeAlias(source.symbol, source.mint);
215
+ const name = clean(first(flags, "name")) ?? source.name;
216
+ const symbol = clean(first(flags, "symbol")) ?? source.symbol;
217
+ const explicitUri = clean(first(flags, "uri"));
218
+ const image = clean(first(flags, "image"));
219
+ const description = clean(first(flags, "description"));
220
+ const website = clean(first(flags, "website"));
221
+ const twitter = clean(first(flags, "twitter"));
222
+ const telegram = clean(first(flags, "telegram"));
223
+ const video = clean(first(flags, "video"));
224
+ const showName = flags.has("hide-name")
225
+ ? false
226
+ : flags.has("show-name")
227
+ ? first(flags, "show-name") !== "false"
228
+ : (source.showName ?? true);
229
+
230
+ const overrides = [
231
+ ["alias", clean(first(flags, "alias"))],
232
+ ["name", clean(first(flags, "name"))],
233
+ ["symbol", clean(first(flags, "symbol"))],
234
+ ["uri", explicitUri],
235
+ ["image", image],
236
+ ["description", description],
237
+ ["website", website],
238
+ ["twitter", twitter],
239
+ ["telegram", telegram],
240
+ ["video", video],
241
+ [
242
+ "showName",
243
+ flags.has("show-name") || flags.has("hide-name") ? "set" : undefined,
244
+ ],
245
+ ]
246
+ .filter(([, value]) => value != null)
247
+ .map(([key]) => key!);
248
+
249
+ const hasJsonFieldOverrides = Boolean(
250
+ image ||
251
+ description ||
252
+ website ||
253
+ twitter ||
254
+ telegram ||
255
+ video ||
256
+ flags.has("show-name") ||
257
+ flags.has("hide-name"),
258
+ );
259
+ const shouldMergeUpload =
260
+ !explicitUri &&
261
+ Boolean(
262
+ clean(first(flags, "name")) ||
263
+ clean(first(flags, "symbol")) ||
264
+ hasJsonFieldOverrides,
265
+ );
266
+
267
+ if (explicitUri && hasJsonFieldOverrides) {
268
+ throw new Error(
269
+ "--uri already defines metadata JSON. Do not combine it with --image/--description/--website/--twitter/--telegram/--video/--show-name/--hide-name; put those fields in the supplied URI instead.",
270
+ );
271
+ }
272
+
273
+ let uri = explicitUri ?? source.uri;
274
+ let metadataMode: PumpVampCliResult["resolved"]["metadataMode"] = explicitUri
275
+ ? "override-uri"
276
+ : "source-uri";
277
+
278
+ if (shouldMergeUpload) {
279
+ const imageFile = await resolveImagePath(image, source.image);
280
+ try {
281
+ const uploaded = await uploadPumpMetadata(
282
+ {
283
+ imagePath: imageFile.path,
284
+ name,
285
+ symbol,
286
+ description:
287
+ description ?? source.description ?? `${name} (${symbol})`,
288
+ website: website ?? source.website ?? undefined,
289
+ twitter: twitter ?? source.twitter ?? undefined,
290
+ telegram: telegram ?? source.telegram ?? undefined,
291
+ video: video ?? source.video ?? undefined,
292
+ showName,
293
+ },
294
+ {
295
+ provider: metadataProvider(flags),
296
+ },
297
+ );
298
+ uri = uploaded.metadataUri;
299
+ metadataMode = "merged-upload";
300
+ report("pump vamp metadata uploaded", {
301
+ provider: uploaded.provider,
302
+ metadataUri: uploaded.metadataUri,
303
+ imageUri: uploaded.imageUri ?? null,
304
+ });
305
+ } finally {
306
+ imageFile.cleanup();
307
+ }
308
+ }
309
+
310
+ report("pump vamp source", {
311
+ mint: source.mint,
312
+ metadataKind: source.metadataKind,
313
+ sourceName: source.name,
314
+ sourceSymbol: source.symbol,
315
+ sourceUri: source.uri,
316
+ metadataJsonFetched: source.json != null,
317
+ });
318
+ report("pump vamp resolved", {
319
+ alias,
320
+ name,
321
+ symbol,
322
+ uri,
323
+ metadataMode,
324
+ overrides,
325
+ cashback: true,
326
+ });
327
+
328
+ const launchArgv = [
329
+ ...stripVampArgs(argv),
330
+ "--alias",
331
+ alias,
332
+ "--name",
333
+ name,
334
+ "--symbol",
335
+ symbol,
336
+ "--uri",
337
+ uri,
338
+ "--cashback",
339
+ ];
340
+
341
+ const launch = await runPumpTokenLaunchFromArgs(launchArgv, options);
342
+
343
+ report("pump vamp result", {
344
+ sourceMint: source.mint,
345
+ newMint: launch.token.mint,
346
+ alias,
347
+ cashback: true,
348
+ live: launch.live,
349
+ });
350
+
351
+ return {
352
+ source,
353
+ resolved: {
354
+ alias,
355
+ name,
356
+ symbol,
357
+ uri,
358
+ metadataMode,
359
+ overrides,
360
+ },
361
+ launch,
362
+ };
363
+ }